2024-09-04 21:48:42 +02:00
|
|
|
import { Pixmap } from "rkgk/haku.js";
|
|
|
|
import { OnlineUsers } from "rkgk/online-users.js";
|
2024-08-10 23:13:20 +02:00
|
|
|
|
|
|
|
export class Chunk {
|
|
|
|
constructor(size) {
|
2024-08-15 20:01:23 +02:00
|
|
|
this.pixmap = new Pixmap(size, size);
|
2024-08-10 23:13:20 +02:00
|
|
|
this.canvas = new OffscreenCanvas(size, size);
|
|
|
|
this.ctx = this.canvas.getContext("2d");
|
2024-09-03 22:16:28 +02:00
|
|
|
this.renderDirty = false;
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|
2024-08-15 20:01:23 +02:00
|
|
|
|
2025-06-30 00:48:49 +02:00
|
|
|
destroy() {
|
|
|
|
this.pixmap.destroy();
|
|
|
|
}
|
|
|
|
|
2024-08-15 20:01:23 +02:00
|
|
|
syncFromPixmap() {
|
2024-09-03 22:16:28 +02:00
|
|
|
this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
|
2024-08-15 20:01:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
syncToPixmap() {
|
|
|
|
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
2024-09-03 22:16:28 +02:00
|
|
|
this.pixmap.getImageData().data.set(imageData.data, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
markModified() {
|
|
|
|
this.renderDirty = true;
|
2024-08-15 20:01:23 +02:00
|
|
|
}
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|
|
|
|
|
2025-06-30 00:48:49 +02:00
|
|
|
let layerIdCounter = 0;
|
2024-08-10 23:13:20 +02:00
|
|
|
|
2025-06-30 00:48:49 +02:00
|
|
|
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);
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|
|
|
|
|
2025-06-30 00:48:49 +02:00
|
|
|
destroy() {
|
|
|
|
for (let { chunk } of this.chunks.values()) {
|
|
|
|
chunk.destroy();
|
|
|
|
}
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getChunk(x, y) {
|
2025-06-30 00:48:49 +02:00
|
|
|
return this.chunks.get(chunkKey(x, y))?.chunk;
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getOrCreateChunk(x, y) {
|
2025-06-30 00:48:49 +02:00
|
|
|
let key = chunkKey(x, y);
|
|
|
|
if (this.chunks.has(key)) {
|
|
|
|
return this.chunks.get(key)?.chunk;
|
2024-08-10 23:13:20 +02:00
|
|
|
} else {
|
2025-06-30 00:48:49 +02:00
|
|
|
if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null;
|
|
|
|
|
2024-08-10 23:13:20 +02:00
|
|
|
let chunk = new Chunk(this.chunkSize);
|
2025-06-30 00:48:49 +02:00
|
|
|
this.chunks.set(key, { x, y, chunk });
|
2024-08-10 23:13:20 +02:00
|
|
|
return chunk;
|
|
|
|
}
|
|
|
|
}
|
2025-06-30 00:48:49 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2024-08-10 23:13:20 +02:00
|
|
|
}
|