189 lines
6.1 KiB
JavaScript
189 lines
6.1 KiB
JavaScript
import { createSocketConnection } from "./infrastructure/socket-client.js";
|
|
import { checkServer } from "./infrastructure/api.js";
|
|
import { createEventBus } from "./infrastructure/event-bus.js";
|
|
import { createKeyboard } from "./infrastructure/keyboard.js";
|
|
import { createAppState } from "./domain/app-state.js";
|
|
import { createCanvasRenderer } from "./ui/canvas-renderer.js";
|
|
import { createColorPalette } from "./ui/color-palette.js";
|
|
import { startCooldownDisplay } from "./ui/cooldown-display.js";
|
|
import { createCanvasViewport } from "./ui/canvas-viewport.js";
|
|
import { createToolManager } from "./ui/tool-manager.js";
|
|
import { createPaintTool } from "./tools/paint-tool.js";
|
|
import { createHandTool } from "./tools/hand-tool.js";
|
|
import { setPixel } from "./domain/canvas-state.js";
|
|
import { getCanvasCoords } from "./domain/coords.js";
|
|
|
|
const els = {
|
|
canvas: document.getElementById("canvas"),
|
|
viewport: document.getElementById("canvas-viewport"),
|
|
status: document.getElementById("connection-status"),
|
|
countdown: document.getElementById("countdown"),
|
|
placeBtn: document.getElementById("place-pixel"),
|
|
soldiers: document.getElementById("current-soldiers"),
|
|
coords: document.getElementById("coords"),
|
|
zoomIn: document.getElementById("zoom-in"),
|
|
zoomOut: document.getElementById("zoom-out"),
|
|
zoomLabel: document.getElementById("zoom-level"),
|
|
modePaint: document.getElementById("mode-paint"),
|
|
modeHand: document.getElementById("mode-hand"),
|
|
colorPicker: document.getElementById("color-picker"),
|
|
colorSpan: document.getElementById("current-color-span"),
|
|
saveCanvas: document.getElementById("save-canvas"),
|
|
};
|
|
|
|
const savedDisplay = els.canvas.style.display;
|
|
els.canvas.style.display = "none";
|
|
|
|
const appState = createAppState({
|
|
canvasPixels: [],
|
|
lastPlacementTime: parseInt(localStorage.getItem("lastPixelTime") || "0"),
|
|
selectedColor: localStorage.getItem("currentColor") || "#000000",
|
|
soldierCount: 0,
|
|
});
|
|
|
|
appState.subscribe("lastPlacementTime", (val) => {
|
|
localStorage.setItem("lastPixelTime", val.toString());
|
|
});
|
|
|
|
appState.subscribe("selectedColor", (val) => {
|
|
localStorage.setItem("currentColor", val);
|
|
});
|
|
|
|
const bus = createEventBus();
|
|
const keyboard = createKeyboard();
|
|
const renderer = createCanvasRenderer(els.canvas);
|
|
|
|
createColorPalette({
|
|
colorPicker: els.colorPicker,
|
|
colorSpan: els.colorSpan,
|
|
appState,
|
|
});
|
|
|
|
startCooldownDisplay({
|
|
countdown: els.countdown,
|
|
placeBtn: els.placeBtn,
|
|
appState,
|
|
});
|
|
|
|
createCanvasViewport({
|
|
canvas: els.canvas,
|
|
viewport: els.viewport,
|
|
zoomIn: els.zoomIn,
|
|
zoomOut: els.zoomOut,
|
|
zoomLabel: els.zoomLabel,
|
|
});
|
|
|
|
const updateModeButtons = (name) => {
|
|
els.modePaint.classList.toggle("ring-2", name === "paint");
|
|
els.modePaint.classList.toggle("ring-offset-2", name === "paint");
|
|
els.modePaint.classList.toggle("ring-cyan-400", name === "paint");
|
|
els.modeHand.classList.toggle("ring-2", name === "hand");
|
|
els.modeHand.classList.toggle("ring-offset-2", name === "hand");
|
|
els.modeHand.classList.toggle("ring-cyan-400", name === "hand");
|
|
};
|
|
|
|
const toolManager = createToolManager({
|
|
canvas: els.canvas,
|
|
viewport: els.viewport,
|
|
onActiveChange: updateModeButtons,
|
|
});
|
|
|
|
toolManager.register(
|
|
createPaintTool({ canvas: els.canvas, renderer, appState, bus }),
|
|
);
|
|
toolManager.register(createHandTool({ viewport: els.viewport }));
|
|
toolManager.setActive("paint");
|
|
|
|
els.modePaint.addEventListener("click", () => toolManager.setActive("paint"));
|
|
els.modeHand.addEventListener("click", () => toolManager.setActive("hand"));
|
|
els.placeBtn.addEventListener("click", () => toolManager.confirm());
|
|
|
|
keyboard.register("Enter", "confirm-placement");
|
|
keyboard.register("Escape", "cancel-preview");
|
|
keyboard.register(" ", "hold-hand-mode", { hold: true });
|
|
|
|
keyboard.onAction("confirm-placement", () => toolManager.confirm());
|
|
keyboard.onAction("cancel-preview", () => toolManager.cancel());
|
|
keyboard.onAction("hold-hand-mode:down", () => toolManager.holdTool("hand"));
|
|
keyboard.onAction("hold-hand-mode:up", () => toolManager.releaseTool());
|
|
|
|
els.canvas.addEventListener("mousemove", (event) => {
|
|
const { x, y } = getCanvasCoords(event, els.canvas);
|
|
els.coords.textContent = `${x}, ${y}`;
|
|
});
|
|
|
|
els.saveCanvas.addEventListener("click", () => {
|
|
const a = document.createElement("a");
|
|
a.href = renderer.toDataURL();
|
|
a.download = "canvas.png";
|
|
a.click();
|
|
});
|
|
|
|
const showStatus = (message, isError) => {
|
|
els.status.textContent = message;
|
|
els.status.className = isError
|
|
? "text-red-500 text-sm"
|
|
: "text-green-500 text-sm";
|
|
};
|
|
|
|
els.soldiers.addEventListener("animationend", () => {
|
|
els.soldiers.classList.remove("soldier-pop");
|
|
});
|
|
|
|
appState.subscribe("soldierCount", (count, prev) => {
|
|
els.soldiers.textContent = count;
|
|
if (count > prev && prev > 0) {
|
|
els.soldiers.classList.remove("soldier-pop");
|
|
void els.soldiers.offsetWidth;
|
|
els.soldiers.classList.add("soldier-pop");
|
|
}
|
|
});
|
|
|
|
bus.on("canvas_state", (data) => {
|
|
appState.set("canvasPixels", data);
|
|
renderer.drawState(data);
|
|
});
|
|
|
|
bus.on("pixel-updated", (update) => {
|
|
renderer.drawPixel(update.x, update.y, update.color);
|
|
setPixel(appState.get("canvasPixels"), update.x, update.y, update.color);
|
|
});
|
|
|
|
bus.on("current_soldiers", (count) => {
|
|
appState.set("soldierCount", count);
|
|
});
|
|
|
|
bus.on("connect", () => {
|
|
els.canvas.style.display = savedDisplay;
|
|
showStatus("Connected", false);
|
|
});
|
|
|
|
bus.on("disconnect", () => {
|
|
showStatus("Disconnected — reconnecting...", true);
|
|
});
|
|
|
|
bus.on("error", (message) => showStatus(message, true));
|
|
|
|
checkServer()
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error("Server unavailable");
|
|
|
|
const socket = createSocketConnection();
|
|
|
|
socket.on("connect", () => bus.emit("connect"));
|
|
socket.on("canvas_state", (data) => bus.emit("canvas_state", data));
|
|
socket.on("pixel-updated", (data) => bus.emit("pixel-updated", data));
|
|
socket.on("current_soldiers", (count) =>
|
|
bus.emit("current_soldiers", count),
|
|
);
|
|
socket.on("error", (message) => bus.emit("error", message));
|
|
socket.on("disconnect", () => bus.emit("disconnect"));
|
|
|
|
bus.on("pixel-placed", (update) => {
|
|
socket.emit("place-pixel", update);
|
|
});
|
|
})
|
|
.catch(() => {
|
|
showStatus("Cannot connect to server", true);
|
|
});
|