rkgk/static/online-users.js
リキ萌 bff899c9c0 removing server-side brush rendering
brush rendering is now completely client-side.
the server only receives edits the client would like to do, in the form of PNG images of chunks, that are then composited onto the wall

known issue: it is possible to brush up against the current 256 chunk edit limit pretty easily.
I'm not sure it can be solved very easily though. the perfect solution would involve splitting up the interaction into multiple edits, and I tried to do that, but there's a noticable stutter for some reason that I haven't managed to track down yet.
so it'll be kinda crap for the time being.
2025-06-30 18:55:53 +02:00

174 lines
5.5 KiB
JavaScript

import { ContKind, Haku } from "rkgk/haku.js";
import { renderToChunksInArea, dotterRenderArea } from "rkgk/painter.js";
import { Layer } from "rkgk/wall.js";
export class User {
nickname = "";
brush = "";
reticle = null;
isBrushOk = false;
simulation = null;
scratchLayer = null;
constructor(wallInfo, nickname) {
this.nickname = nickname;
this.haku = new Haku(wallInfo.hakuLimits);
}
destroy() {
console.info("destroying user", this.nickname);
this.haku.destroy();
}
setBrush(brush) {
console.groupCollapsed("setBrush", this.nickname);
let compileResult = this.haku.setBrush(brush);
console.info("compiling brush complete", compileResult);
console.groupEnd();
this.isBrushOk = compileResult.status == "ok";
return compileResult;
}
renderBrushToChunks(wall, x, y) {
console.groupCollapsed("renderBrushToChunks", this.nickname);
let result = this.painter.renderBrushToWall(this.haku, x, y, wall);
console.info("rendering brush to chunks complete");
console.groupEnd();
return result;
}
simulate(wall, interactions) {
console.group("simulate", this.nickname);
for (let interaction of interactions) {
if (interaction.kind == "setBrush") {
this.simulation = null;
this.setBrush(interaction.brush);
}
if (this.isBrushOk) {
if (this.simulation == null) {
console.info("no simulation -- beginning brush");
this.simulation = { renderArea: { left: 0, top: 0, right: 0, bottom: 0 } };
this.haku.beginBrush();
}
if (interaction.kind == "dotter" && this.#expectContKind(ContKind.Dotter)) {
let dotter = {
fromX: interaction.from.x,
fromY: interaction.from.y,
toX: interaction.to.x,
toY: interaction.to.y,
num: interaction.num,
};
this.haku.contDotter(dotter);
this.simulation.renderArea = dotterRenderArea(wall, dotter);
}
if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
renderToChunksInArea(
this.getScratchLayer(wall),
this.simulation.renderArea,
(pixmap, translationX, translationY) => {
return this.haku.contScribble(pixmap, translationX, translationY);
},
);
console.info("ended simulation");
this.simulation = null;
}
}
}
console.groupEnd();
}
#expectContKind(kind) {
if (this.haku.expectedContKind() == kind) {
return true;
} else {
console.error(`expected cont kind: ${kind}`);
return false;
}
}
getStats(wallInfo) {
return {
astSize: this.haku.astSize,
astSizeMax: wallInfo.hakuLimits.ast_capacity,
fuel: wallInfo.hakuLimits.fuel - this.haku.remainingFuel,
fuelMax: wallInfo.hakuLimits.fuel,
memory: this.haku.usedMemory,
memoryMax: wallInfo.hakuLimits.memory,
};
}
getScratchLayer(wall) {
if (this.scratchLayer == null) {
this.scratchLayer = wall.addLayer(
new Layer({
name: `scratch ${this.nickname}`,
chunkSize: wall.chunkSize,
chunkLimit: wall.maxEditSize,
}),
);
}
return this.scratchLayer;
}
// Returns the scratch layer committed to the wall, so that the caller may do additional
// processing with the completed layer (i.e. send to the server.)
// The layer has to be .destroy()ed once you're done working with it.
commitScratchLayer(wall) {
if (this.scratchLayer != null) {
wall.mainLayer.compositeAlpha(this.scratchLayer);
wall.removeLayer(this.scratchLayer);
let scratchLayer = this.scratchLayer;
this.scratchLayer = null;
return scratchLayer;
} else {
console.error("commitScratchLayer without an active scratch layer", this.nickname);
}
}
}
export class OnlineUsers extends EventTarget {
#wallInfo;
#users = new Map();
constructor(wallInfo) {
super();
this.#wallInfo = wallInfo;
}
addUser(sessionId, { nickname, brush }) {
if (!this.#users.has(sessionId)) {
console.info("user added", sessionId, nickname);
let user = new User(this.#wallInfo, nickname);
user.setBrush(brush);
this.#users.set(sessionId, user);
return user;
} else {
console.info("user already exists", sessionId, nickname);
return this.#users.get(sessionId);
}
}
getUser(sessionId) {
return this.#users.get(sessionId);
}
removeUser(sessionId) {
if (this.#users.has(sessionId)) {
let user = this.#users.get(sessionId);
user.destroy();
console.info("user removed", sessionId, user.nickname);
// TODO: Cleanup reticles
this.#users.delete(sessionId);
}
}
}