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); } }