rkgk/static/index.js

258 lines
8.6 KiB
JavaScript
Raw Normal View History

2024-08-10 23:13:20 +02:00
import { Wall } from "./wall.js";
import {
getLoginSecret,
getUserId,
isUserLoggedIn,
newSession,
registerUser,
waitForLogin,
} from "./session.js";
2024-08-10 23:13:20 +02:00
import { debounce } from "./framework.js";
2024-08-15 20:01:23 +02:00
import { ReticleCursor } from "./reticle-renderer.js";
const updateInterval = 1000 / 60;
2024-08-10 23:10:03 +02:00
2024-08-10 23:13:20 +02:00
let main = document.querySelector("main");
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
let brushEditor = main.querySelector("rkgk-brush-editor");
let welcome = main.querySelector("rkgk-welcome");
let connectionStatus = main.querySelector("rkgk-connection-status");
2024-08-10 23:10:03 +02:00
document.getElementById("js-loading").remove();
2024-08-10 23:13:20 +02:00
reticleRenderer.connectViewport(canvasRenderer.viewport);
2024-08-10 23:10:03 +02:00
function updateUrl(session, viewport) {
let url = new URL(window.location);
url.hash = `${session.wallId}&x=${Math.floor(viewport.panX)}&y=${Math.floor(viewport.panY)}&zoom=${viewport.zoomLevel}`;
history.replaceState(null, "", url);
}
function readUrl() {
let url = new URL(window.location);
let fragments = url.hash.substring(1).split("&");
let wallId = null;
let viewport = { x: 0, y: 0, zoom: 0 };
if (fragments.length == 0) return { wallId, viewport };
if (fragments[0].startsWith("wall_") && fragments[0].length == 48) {
wallId = fragments[0];
}
for (let i = 1; i < fragments.length; ++i) {
let pair = fragments[i].split("=");
if (pair.length != 2) continue;
let [key, value] = pair;
try {
if (key == "x") viewport.x = parseFloat(value);
if (key == "y") viewport.y = parseFloat(value);
if (key == "zoom") viewport.zoom = parseFloat(value);
} catch (error) {
console.error(`broken fragment url value: ${key}=${value}`);
}
}
return { wallId, viewport };
}
2024-08-10 23:13:20 +02:00
// In the background, connect to the server.
(async () => {
console.info("checking for user registration status");
if (!isUserLoggedIn()) {
await welcome.show({
async onRegister(nickname) {
return await registerUser(nickname);
},
});
}
connectionStatus.showLoggingIn();
2024-08-10 23:13:20 +02:00
await waitForLogin();
console.info("login ready! starting session");
2024-08-10 23:10:03 +02:00
let urlData = readUrl();
canvasRenderer.viewport.panX = urlData.viewport.x;
canvasRenderer.viewport.panY = urlData.viewport.y;
canvasRenderer.viewport.zoomLevel = urlData.viewport.zoom;
let session = await newSession({
userId: getUserId(),
secret: getLoginSecret(),
wallId: urlData.wallId ?? localStorage.getItem("rkgk.mostRecentWallId"),
userInit: {
brush: brushEditor.code,
},
onError(error) {
connectionStatus.showError(error.source);
},
async onDisconnect() {
let duration = 5000 + Math.random() * 1000;
while (true) {
console.info("waiting a bit for the server to come back up", duration);
await connectionStatus.showDisconnected(duration);
try {
console.info("trying to reconnect");
let response = await fetch("/auto-reload/back-up");
if (response.status == 200) {
window.location.reload();
break;
}
} catch (e) {}
duration = duration * 1.618033989 + Math.random() * 1000;
}
},
});
2024-08-10 23:13:20 +02:00
localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
2024-08-10 23:10:03 +02:00
connectionStatus.hideLoggingIn();
updateUrl(session, canvasRenderer.viewport);
2024-08-15 20:01:23 +02:00
let wall = new Wall(session.wallInfo);
2024-08-10 23:13:20 +02:00
canvasRenderer.initialize(wall);
2024-08-10 23:10:03 +02:00
2024-08-10 23:13:20 +02:00
for (let onlineUser of session.wallInfo.online) {
2024-08-15 20:01:23 +02:00
wall.onlineUsers.addUser(onlineUser.sessionId, {
nickname: onlineUser.nickname,
brush: onlineUser.init.brush,
});
2024-08-10 23:10:03 +02:00
}
2024-08-15 20:01:23 +02:00
let currentUser = wall.onlineUsers.getUser(session.sessionId);
2024-08-10 23:13:20 +02:00
session.addEventListener("error", (event) => console.error(event));
2024-08-15 20:01:23 +02:00
session.addEventListener("wallEvent", (event) => {
let wallEvent = event.wallEvent;
if (wallEvent.sessionId != session.sessionId) {
if (wallEvent.kind.event == "join") {
wall.onlineUsers.addUser(wallEvent.sessionId, {
nickname: wallEvent.kind.nickname,
brush: wallEvent.kind.init.brush,
});
}
let user = wall.onlineUsers.getUser(wallEvent.sessionId);
if (user == null) {
console.warn("received event for an unknown user", wallEvent);
return;
}
if (wallEvent.kind.event == "leave") {
if (user.reticle != null) {
reticleRenderer.removeReticle(user.reticle);
}
wall.onlineUsers.removeUser(wallEvent.sessionId);
}
if (wallEvent.kind.event == "cursor") {
if (user.reticle == null) {
user.reticle = new ReticleCursor(
wall.onlineUsers.getUser(wallEvent.sessionId).nickname,
);
reticleRenderer.addReticle(user.reticle);
}
let { x, y } = wallEvent.kind.position;
user.reticle.setCursor(x, y);
}
if (wallEvent.kind.event == "setBrush") {
user.setBrush(wallEvent.kind.brush);
}
if (wallEvent.kind.event == "plot") {
for (let { x, y } of wallEvent.kind.points) {
user.renderBrushToChunks(wall, x, y);
}
}
2024-08-10 23:13:20 +02:00
}
});
let sendViewportUpdate = debounce(updateInterval, () => {
2024-08-15 20:01:23 +02:00
let visibleRect = canvasRenderer.getVisibleChunkRect();
session.sendViewport(visibleRect);
});
2024-08-15 20:01:23 +02:00
canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate);
sendViewportUpdate();
session.addEventListener("chunks", async (event) => {
let { chunkInfo, chunkData, hasMore } = event;
2024-08-15 20:01:23 +02:00
console.debug("received data for chunks", {
chunkInfo,
2024-08-15 20:01:23 +02:00
chunkDataSize: chunkData.size,
});
let updatePromises = [];
2024-08-15 20:01:23 +02:00
for (let info of event.chunkInfo) {
if (info.length > 0) {
let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp");
updatePromises.push(
createImageBitmap(blob).then((bitmap) => {
let chunk = wall.getOrCreateChunk(info.position.x, info.position.y);
chunk.ctx.globalCompositeOperation = "copy";
chunk.ctx.drawImage(bitmap, 0, 0);
chunk.syncToPixmap();
}),
);
2024-08-15 20:01:23 +02:00
}
}
await Promise.all(updatePromises);
if (hasMore) {
console.info("more chunks are pending; requesting more");
session.sendMoreChunks();
}
2024-08-15 20:01:23 +02:00
});
2024-08-10 23:13:20 +02:00
2024-08-15 20:01:23 +02:00
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y));
2024-08-10 23:13:20 +02:00
canvasRenderer.addEventListener(".cursor", async (event) => {
reportCursor(event.x, event.y);
});
2024-08-15 20:01:23 +02:00
let plotQueue = [];
async function flushPlotQueue() {
let points = plotQueue.splice(0, plotQueue.length);
if (points.length != 0) {
session.sendPlot(points);
2024-08-10 23:13:20 +02:00
}
2024-08-15 20:01:23 +02:00
}
setInterval(flushPlotQueue, updateInterval);
canvasRenderer.addEventListener(".paint", async (event) => {
plotQueue.push({ x: event.x, y: event.y });
2024-08-19 23:56:11 +02:00
if (currentUser.isBrushOk) {
brushEditor.resetErrors();
let result = currentUser.renderBrushToChunks(wall, event.x, event.y);
if (result.status == "error") {
brushEditor.renderHakuResult(
result.phase == "eval" ? "Evaluation" : "Rendering",
result.result,
);
}
}
2024-08-15 20:01:23 +02:00
});
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
canvasRenderer.addEventListener(".viewportUpdateEnd", () =>
updateUrl(session, canvasRenderer.viewport),
);
2024-08-15 20:01:23 +02:00
2024-08-19 23:56:11 +02:00
brushEditor.renderHakuResult("Compilation", currentUser.setBrush(brushEditor.code));
2024-08-15 20:01:23 +02:00
brushEditor.addEventListener(".codeChanged", async () => {
flushPlotQueue();
2024-08-19 23:56:11 +02:00
brushEditor.renderHakuResult("Compilation", currentUser.setBrush(brushEditor.code));
2024-08-15 20:01:23 +02:00
session.sendSetBrush(brushEditor.code);
2024-08-10 23:13:20 +02:00
});
session.eventLoop();
})();