rkgk/static/wall.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

143 lines
3.7 KiB
JavaScript

import { Pixmap } from "rkgk/haku.js";
import { OnlineUsers } from "rkgk/online-users.js";
export class Chunk {
constructor(size) {
this.pixmap = new Pixmap(size, size);
this.canvas = new OffscreenCanvas(size, size);
this.ctx = this.canvas.getContext("2d");
this.renderDirty = false;
}
destroy() {
this.pixmap.destroy();
}
syncFromPixmap() {
this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
}
syncToPixmap() {
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
this.pixmap.getImageData().data.set(imageData.data, 0);
}
markModified() {
this.renderDirty = true;
}
}
let layerIdCounter = 0;
export class Layer {
chunks = new Map();
id = layerIdCounter++;
constructor({ name, chunkSize, chunkLimit }) {
this.name = name;
this.chunkSize = chunkSize;
this.chunkLimit = chunkLimit;
console.info("created layer", this.id, this.name);
}
destroy() {
for (let { chunk } of this.chunks.values()) {
chunk.destroy();
}
}
getChunk(x, y) {
return this.chunks.get(chunkKey(x, y))?.chunk;
}
getOrCreateChunk(x, y) {
let key = chunkKey(x, y);
if (this.chunks.has(key)) {
return this.chunks.get(key)?.chunk;
} else {
if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null;
let chunk = new Chunk(this.chunkSize);
this.chunks.set(key, { x, y, chunk });
return chunk;
}
}
compositeAlpha(src) {
for (let { x, y, chunk: srcChunk } of src.chunks.values()) {
srcChunk.syncFromPixmap();
let dstChunk = this.getOrCreateChunk(x, y);
if (dstChunk == null) continue;
dstChunk.ctx.globalCompositeOperation = "source-over";
dstChunk.ctx.drawImage(srcChunk.canvas, 0, 0);
dstChunk.syncToPixmap();
dstChunk.markModified();
}
}
async toEdits() {
let edits = [];
let start = performance.now();
for (let { x, y, chunk } of this.chunks.values()) {
edits.push({
chunk: { x, y },
data: chunk.canvas.convertToBlob({ type: "image/png" }),
});
}
for (let edit of edits) {
edit.data = await edit.data;
}
let end = performance.now();
console.debug("toEdits done", end - start);
return edits;
}
}
export function chunkKey(x, y) {
return `${x},${y}`;
}
export class Wall {
layers = []; // do not modify directly; only read
#layersById = new Map();
constructor(wallInfo) {
this.chunkSize = wallInfo.chunkSize;
this.paintArea = wallInfo.paintArea;
this.maxEditSize = wallInfo.maxEditSize;
this.onlineUsers = new OnlineUsers(wallInfo);
this.mainLayer = new Layer({ name: "main", chunkSize: this.chunkSize });
this.addLayer(this.mainLayer);
}
addLayer(layer) {
if (!this.#layersById.get(layer.id)) {
this.layers.push(layer);
this.#layersById.set(layer.id, layer);
} else {
console.warn("attempt to add layer more than once", layer);
}
return layer;
}
removeLayer(layer) {
if (this.#layersById.delete(layer.id)) {
let index = this.layers.findIndex((x) => x == layer);
this.layers.splice(index, 1);
} else {
console.warn("attempt to remove layer more than once", layer);
}
}
getLayerById(id) {
return this.#layersById.get(id);
}
}