sync
This commit is contained in:
parent
26ba098183
commit
2f7bcbb14e
30 changed files with 1691 additions and 315 deletions
|
@ -34,6 +34,13 @@ class CanvasRenderer extends HTMLElement {
|
|||
this.#render();
|
||||
}
|
||||
|
||||
getWindowSize() {
|
||||
return {
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight,
|
||||
};
|
||||
}
|
||||
|
||||
#render() {
|
||||
// NOTE: We should probably render on-demand only when it's needed.
|
||||
requestAnimationFrame(() => this.#render());
|
||||
|
@ -41,6 +48,19 @@ class CanvasRenderer extends HTMLElement {
|
|||
this.#renderWall();
|
||||
}
|
||||
|
||||
getVisibleRect() {
|
||||
return this.viewport.getVisibleRect(this.getWindowSize());
|
||||
}
|
||||
|
||||
getVisibleChunkRect() {
|
||||
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
|
||||
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
|
||||
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
|
||||
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
|
||||
let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
|
||||
return { left, top, right, bottom };
|
||||
}
|
||||
|
||||
#renderWall() {
|
||||
if (this.wall == null) {
|
||||
console.debug("wall is not available, skipping rendering");
|
||||
|
@ -55,10 +75,7 @@ class CanvasRenderer extends HTMLElement {
|
|||
this.ctx.scale(this.viewport.zoom, this.viewport.zoom);
|
||||
this.ctx.translate(-this.viewport.panX, -this.viewport.panY);
|
||||
|
||||
let visibleRect = this.viewport.getVisibleRect({
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight,
|
||||
});
|
||||
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
|
||||
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
|
||||
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
|
||||
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
|
||||
|
@ -88,10 +105,7 @@ class CanvasRenderer extends HTMLElement {
|
|||
let [x, y] = this.viewport.toViewportSpace(
|
||||
event.clientX - this.clientLeft,
|
||||
event.offsetY - this.clientTop,
|
||||
{
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight,
|
||||
},
|
||||
this.getWindowSize(),
|
||||
);
|
||||
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
|
||||
}
|
||||
|
@ -127,10 +141,7 @@ class CanvasRenderer extends HTMLElement {
|
|||
|
||||
async #paintingBehaviour() {
|
||||
const paint = (x, y) => {
|
||||
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, {
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight,
|
||||
});
|
||||
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize());
|
||||
this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
|
||||
};
|
||||
|
||||
|
|
|
@ -118,9 +118,16 @@ export class Haku {
|
|||
#pBrush = 0;
|
||||
#brushCode = null;
|
||||
|
||||
constructor() {
|
||||
this.#pInstance = w.haku_instance_new();
|
||||
constructor(limits) {
|
||||
let pLimits = w.haku_limits_new();
|
||||
for (let name of Object.keys(limits)) {
|
||||
w[`haku_limits_set_${name}`](pLimits, limits[name]);
|
||||
}
|
||||
|
||||
this.#pInstance = w.haku_instance_new(pLimits);
|
||||
this.#pBrush = w.haku_brush_new();
|
||||
|
||||
w.haku_limits_destroy(pLimits);
|
||||
}
|
||||
|
||||
setBrush(code) {
|
||||
|
@ -166,18 +173,7 @@ export class Haku {
|
|||
return { status: "ok" };
|
||||
}
|
||||
|
||||
renderBrush(pixmap, translationX, translationY) {
|
||||
let statusCode = w.haku_render_brush(
|
||||
this.#pInstance,
|
||||
this.#pBrush,
|
||||
pixmap.ptr,
|
||||
// If we ever want to detect which pixels were touched (USING A SHADER.), we can use
|
||||
// this to rasterize the brush _twice_, and then we can detect which pixels are the same
|
||||
// between the two pixmaps.
|
||||
0,
|
||||
translationX,
|
||||
translationY,
|
||||
);
|
||||
#statusCodeToResultObject(statusCode) {
|
||||
if (!w.haku_is_ok(statusCode)) {
|
||||
if (w.haku_is_exception(statusCode)) {
|
||||
return {
|
||||
|
@ -196,8 +192,22 @@ export class Haku {
|
|||
message: readCString(w.haku_status_string(statusCode)),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return { status: "ok" };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: "ok" };
|
||||
evalBrush() {
|
||||
return this.#statusCodeToResultObject(w.haku_eval_brush(this.#pInstance, this.#pBrush));
|
||||
}
|
||||
|
||||
renderValue(pixmap, translationX, translationY) {
|
||||
return this.#statusCodeToResultObject(
|
||||
w.haku_render_value(this.#pInstance, pixmap.ptr, translationX, translationY),
|
||||
);
|
||||
}
|
||||
|
||||
resetVm() {
|
||||
w.haku_reset_vm(this.#pInstance);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,8 +178,8 @@ rkgk-reticle-renderer {
|
|||
}
|
||||
}
|
||||
|
||||
rkgk-reticle {
|
||||
--color: black;
|
||||
rkgk-reticle-cursor {
|
||||
--color: black; /* Overridden by JavaScript to set a per-user color. */
|
||||
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
|
166
static/index.js
166
static/index.js
|
@ -1,73 +1,165 @@
|
|||
import { Painter } from "./painter.js";
|
||||
import { Wall } from "./wall.js";
|
||||
import { Haku } from "./haku.js";
|
||||
import { getUserId, newSession, waitForLogin } from "./session.js";
|
||||
import { debounce } from "./framework.js";
|
||||
import { ReticleCursor } from "./reticle-renderer.js";
|
||||
|
||||
const updateInterval = 1000 / 60;
|
||||
|
||||
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 haku = new Haku();
|
||||
let painter = new Painter(512);
|
||||
|
||||
reticleRenderer.connectViewport(canvasRenderer.viewport);
|
||||
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.updateTransform());
|
||||
|
||||
// In the background, connect to the server.
|
||||
(async () => {
|
||||
await waitForLogin();
|
||||
console.info("login ready! starting session");
|
||||
|
||||
let session = await newSession(getUserId(), localStorage.getItem("rkgk.mostRecentWallId"));
|
||||
let session = await newSession(getUserId(), localStorage.getItem("rkgk.mostRecentWallId"), {
|
||||
brush: brushEditor.code,
|
||||
});
|
||||
localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
|
||||
|
||||
let wall = new Wall(session.wallInfo.chunkSize);
|
||||
let wall = new Wall(session.wallInfo);
|
||||
canvasRenderer.initialize(wall);
|
||||
|
||||
for (let onlineUser of session.wallInfo.online) {
|
||||
wall.onlineUsers.addUser(onlineUser.sessionId, { nickname: onlineUser.nickname });
|
||||
wall.onlineUsers.addUser(onlineUser.sessionId, {
|
||||
nickname: onlineUser.nickname,
|
||||
brush: onlineUser.init.brush,
|
||||
});
|
||||
}
|
||||
|
||||
let currentUser = wall.onlineUsers.getUser(session.sessionId);
|
||||
|
||||
session.addEventListener("error", (event) => console.error(event));
|
||||
session.addEventListener("action", (event) => {
|
||||
if (event.kind.event == "cursor") {
|
||||
let reticle = reticleRenderer.getOrAddReticle(wall.onlineUsers, event.sessionId);
|
||||
let { x, y } = event.kind.position;
|
||||
reticle.setCursor(x, y);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let compileBrush = () => haku.setBrush(brushEditor.code);
|
||||
compileBrush();
|
||||
brushEditor.addEventListener(".codeChanged", () => compileBrush());
|
||||
let pendingChunks = 0;
|
||||
let chunkDownloadStates = new Map();
|
||||
|
||||
let reportCursor = debounce(1000 / 60, (x, y) => session.reportCursor(x, y));
|
||||
function sendViewportUpdate() {
|
||||
let visibleRect = canvasRenderer.getVisibleChunkRect();
|
||||
session.sendViewport(visibleRect);
|
||||
|
||||
for (let chunkY = visibleRect.top; chunkY < visibleRect.bottom; ++chunkY) {
|
||||
for (let chunkX = visibleRect.left; chunkX < visibleRect.right; ++chunkX) {
|
||||
let key = Wall.chunkKey(chunkX, chunkY);
|
||||
let currentState = chunkDownloadStates.get(key);
|
||||
if (currentState == null) {
|
||||
chunkDownloadStates.set(key, "requested");
|
||||
pendingChunks += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.info("pending chunks after viewport update", pendingChunks);
|
||||
}
|
||||
|
||||
canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate);
|
||||
sendViewportUpdate();
|
||||
|
||||
session.addEventListener("chunks", (event) => {
|
||||
let { chunkInfo, chunkData } = event;
|
||||
|
||||
console.info("received data for chunks", {
|
||||
chunkInfoLength: chunkInfo.length,
|
||||
chunkDataSize: chunkData.size,
|
||||
});
|
||||
|
||||
for (let info of event.chunkInfo) {
|
||||
let key = Wall.chunkKey(info.position.x, info.position.y);
|
||||
if (chunkDownloadStates.get(key) == "requested") {
|
||||
pendingChunks -= 1;
|
||||
}
|
||||
chunkDownloadStates.set(key, "downloaded");
|
||||
|
||||
if (info.length > 0) {
|
||||
let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp");
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y));
|
||||
canvasRenderer.addEventListener(".cursor", async (event) => {
|
||||
reportCursor(event.x, event.y);
|
||||
});
|
||||
|
||||
canvasRenderer.addEventListener(".paint", async (event) => {
|
||||
painter.renderBrush(haku);
|
||||
let imageBitmap = await painter.createImageBitmap();
|
||||
|
||||
let left = event.x - painter.paintArea / 2;
|
||||
let top = event.y - painter.paintArea / 2;
|
||||
|
||||
let leftChunk = Math.floor(left / wall.chunkSize);
|
||||
let topChunk = Math.floor(top / wall.chunkSize);
|
||||
let rightChunk = Math.ceil((left + painter.paintArea) / wall.chunkSize);
|
||||
let bottomChunk = Math.ceil((top + painter.paintArea) / wall.chunkSize);
|
||||
for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
|
||||
for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
|
||||
let chunk = wall.getOrCreateChunk(chunkX, chunkY);
|
||||
let x = Math.floor(-chunkX * wall.chunkSize + left);
|
||||
let y = Math.floor(-chunkY * wall.chunkSize + top);
|
||||
chunk.ctx.drawImage(imageBitmap, x, y);
|
||||
}
|
||||
let plotQueue = [];
|
||||
async function flushPlotQueue() {
|
||||
let points = plotQueue.splice(0, plotQueue.length);
|
||||
if (points.length != 0) {
|
||||
session.sendPlot(points);
|
||||
}
|
||||
imageBitmap.close();
|
||||
}
|
||||
|
||||
setInterval(flushPlotQueue, updateInterval);
|
||||
|
||||
canvasRenderer.addEventListener(".paint", async (event) => {
|
||||
plotQueue.push({ x: event.x, y: event.y });
|
||||
currentUser.renderBrushToChunks(wall, event.x, event.y);
|
||||
});
|
||||
|
||||
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
|
||||
|
||||
currentUser.setBrush(brushEditor.code);
|
||||
brushEditor.addEventListener(".codeChanged", async () => {
|
||||
flushPlotQueue();
|
||||
currentUser.setBrush(brushEditor.code);
|
||||
session.sendSetBrush(brushEditor.code);
|
||||
});
|
||||
|
||||
session.eventLoop();
|
||||
|
|
|
@ -1,12 +1,53 @@
|
|||
export class OnlineUsers extends EventTarget {
|
||||
#users = new Map();
|
||||
import { Haku } from "./haku.js";
|
||||
import { Painter } from "./painter.js";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
export class User {
|
||||
nickname = "";
|
||||
brush = "";
|
||||
reticle = null;
|
||||
|
||||
isBrushOk = false;
|
||||
|
||||
constructor(wallInfo, nickname) {
|
||||
this.nickname = nickname;
|
||||
|
||||
this.haku = new Haku(wallInfo.hakuLimits);
|
||||
this.painter = new Painter(wallInfo.paintArea);
|
||||
}
|
||||
|
||||
addUser(sessionId, userInfo) {
|
||||
this.#users.set(sessionId, userInfo);
|
||||
setBrush(brush) {
|
||||
let compileResult = this.haku.setBrush(brush);
|
||||
this.isBrushOk = compileResult.status == "ok";
|
||||
return compileResult;
|
||||
}
|
||||
|
||||
renderBrushToChunks(wall, x, y) {
|
||||
this.painter.renderBrushToWall(this.haku, x, y, wall);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -14,6 +55,11 @@ export class OnlineUsers extends EventTarget {
|
|||
}
|
||||
|
||||
removeUser(sessionId) {
|
||||
this.#users.delete(sessionId);
|
||||
if (this.#users.has(sessionId)) {
|
||||
let user = this.#users.get(sessionId);
|
||||
console.info("user removed", sessionId, user.nickname);
|
||||
// TODO: Cleanup reticles
|
||||
this.#users.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,36 @@
|
|||
import { Pixmap } from "./haku.js";
|
||||
|
||||
export class Painter {
|
||||
#pixmap;
|
||||
imageBitmap;
|
||||
|
||||
constructor(paintArea) {
|
||||
this.paintArea = paintArea;
|
||||
this.#pixmap = new Pixmap(paintArea, paintArea);
|
||||
}
|
||||
|
||||
async createImageBitmap() {
|
||||
return await createImageBitmap(this.#pixmap.imageData);
|
||||
}
|
||||
renderBrushToWall(haku, centerX, centerY, wall) {
|
||||
let evalResult = haku.evalBrush();
|
||||
if (evalResult.status != "ok") return evalResult;
|
||||
|
||||
renderBrush(haku) {
|
||||
this.#pixmap.clear(0, 0, 0, 0);
|
||||
let result = haku.renderBrush(this.#pixmap, this.paintArea / 2, this.paintArea / 2);
|
||||
let left = centerX - this.paintArea / 2;
|
||||
let top = centerY - this.paintArea / 2;
|
||||
|
||||
return result;
|
||||
let leftChunk = Math.floor(left / wall.chunkSize);
|
||||
let topChunk = Math.floor(top / wall.chunkSize);
|
||||
let rightChunk = Math.ceil((left + this.paintArea) / wall.chunkSize);
|
||||
let bottomChunk = Math.ceil((top + this.paintArea) / wall.chunkSize);
|
||||
for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
|
||||
for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
|
||||
let x = Math.floor(-chunkX * wall.chunkSize + centerX);
|
||||
let y = Math.floor(-chunkY * wall.chunkSize + centerY);
|
||||
let chunk = wall.getOrCreateChunk(chunkX, chunkY);
|
||||
|
||||
let renderResult = haku.renderValue(chunk.pixmap, x, y);
|
||||
if (renderResult.status != "ok") return renderResult;
|
||||
}
|
||||
}
|
||||
haku.resetVm();
|
||||
|
||||
for (let y = topChunk; y < bottomChunk; ++y) {
|
||||
for (let x = leftChunk; x < rightChunk; ++x) {
|
||||
let chunk = wall.getChunk(x, y);
|
||||
chunk.syncFromPixmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
export class Reticle extends HTMLElement {
|
||||
#kind = null;
|
||||
#data = {};
|
||||
render(_viewport, _windowSize) {
|
||||
throw new Error("Reticle.render must be overridden");
|
||||
}
|
||||
}
|
||||
|
||||
export class ReticleCursor extends Reticle {
|
||||
#container;
|
||||
|
||||
constructor(nickname) {
|
||||
|
@ -14,84 +17,60 @@ export class Reticle extends HTMLElement {
|
|||
|
||||
this.#container = this.appendChild(document.createElement("div"));
|
||||
this.#container.classList.add("container");
|
||||
|
||||
this.classList.add("cursor");
|
||||
|
||||
let arrow = this.#container.appendChild(document.createElement("div"));
|
||||
arrow.classList.add("arrow");
|
||||
|
||||
let nickname = this.#container.appendChild(document.createElement("div"));
|
||||
nickname.classList.add("nickname");
|
||||
nickname.textContent = this.nickname;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
let hash = 5381;
|
||||
let hash = 8803;
|
||||
for (let i = 0; i < this.nickname.length; ++i) {
|
||||
hash <<= 5;
|
||||
hash += hash;
|
||||
hash += this.nickname.charCodeAt(i);
|
||||
hash &= 0xffff;
|
||||
hash = (hash << 5) - hash + this.nickname.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return `oklch(70% 0.2 ${(hash / 0xffff) * 360}deg)`;
|
||||
}
|
||||
|
||||
#update(kind, data) {
|
||||
this.#data = data;
|
||||
|
||||
if (kind != this.#kind) {
|
||||
this.classList = "";
|
||||
this.#container.replaceChildren();
|
||||
this.#kind = kind;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new Event(".update"));
|
||||
return `oklch(65% 0.2 ${(hash / 0xffff) * 360}deg)`;
|
||||
}
|
||||
|
||||
setCursor(x, y) {
|
||||
this.#update("cursor", { x, y });
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.dispatchEvent(new Event(".update"));
|
||||
}
|
||||
|
||||
render(viewport, windowSize) {
|
||||
if (!this.rendered) {
|
||||
if (this.#kind == "cursor") {
|
||||
this.classList.add("cursor");
|
||||
|
||||
let arrow = this.#container.appendChild(document.createElement("div"));
|
||||
arrow.classList.add("arrow");
|
||||
|
||||
let nickname = this.#container.appendChild(document.createElement("div"));
|
||||
nickname.classList.add("nickname");
|
||||
nickname.textContent = this.nickname;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#kind == "cursor") {
|
||||
let { x, y } = this.#data;
|
||||
let [viewportX, viewportY] = viewport.toScreenSpace(x, y, windowSize);
|
||||
this.style.transform = `translate(${viewportX}px, ${viewportY}px)`;
|
||||
}
|
||||
|
||||
this.rendered = true;
|
||||
let [viewportX, viewportY] = viewport.toScreenSpace(this.x, this.y, windowSize);
|
||||
this.style.transform = `translate(${viewportX}px, ${viewportY}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rkgk-reticle", Reticle);
|
||||
customElements.define("rkgk-reticle-cursor", ReticleCursor);
|
||||
|
||||
export class ReticleRenderer extends HTMLElement {
|
||||
#reticles = new Map();
|
||||
#reticles = new Set();
|
||||
#reticlesDiv;
|
||||
|
||||
connectedCallback() {
|
||||
this.#reticlesDiv = this.appendChild(document.createElement("div"));
|
||||
this.#reticlesDiv.classList.add("reticles");
|
||||
|
||||
this.updateTransform();
|
||||
let resizeObserver = new ResizeObserver(() => this.updateTransform());
|
||||
this.render();
|
||||
let resizeObserver = new ResizeObserver(() => this.render());
|
||||
resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
connectViewport(viewport) {
|
||||
this.viewport = viewport;
|
||||
this.updateTransform();
|
||||
this.render();
|
||||
}
|
||||
|
||||
getOrAddReticle(onlineUsers, sessionId) {
|
||||
if (this.#reticles.has(sessionId)) {
|
||||
return this.#reticles.get(sessionId);
|
||||
} else {
|
||||
let reticle = new Reticle(onlineUsers.getUser(sessionId).nickname);
|
||||
addReticle(reticle) {
|
||||
if (!this.#reticles.has(reticle)) {
|
||||
reticle.addEventListener(".update", () => {
|
||||
if (this.viewport != null) {
|
||||
reticle.render(this.viewport, {
|
||||
|
@ -100,28 +79,26 @@ export class ReticleRenderer extends HTMLElement {
|
|||
});
|
||||
}
|
||||
});
|
||||
this.#reticles.set(sessionId, reticle);
|
||||
this.#reticles.add(reticle);
|
||||
this.#reticlesDiv.appendChild(reticle);
|
||||
return reticle;
|
||||
}
|
||||
}
|
||||
|
||||
removeReticle(sessionId) {
|
||||
if (this.#reticles.has(sessionId)) {
|
||||
let reticle = this.#reticles.get(sessionId);
|
||||
this.#reticles.delete(sessionId);
|
||||
removeReticle(reticle) {
|
||||
if (this.#reticles.has(reticle)) {
|
||||
this.#reticles.delete(reticle);
|
||||
this.#reticlesDiv.removeChild(reticle);
|
||||
}
|
||||
}
|
||||
|
||||
updateTransform() {
|
||||
render() {
|
||||
if (this.viewport == null) {
|
||||
console.debug("viewport is disconnected, skipping transform update");
|
||||
return;
|
||||
}
|
||||
|
||||
let windowSize = { width: this.clientWidth, height: this.clientHeight };
|
||||
for (let [_, reticle] of this.#reticles) {
|
||||
for (let reticle of this.#reticles.values()) {
|
||||
reticle.render(this.viewport, windowSize);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,8 +85,16 @@ class Session extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
async #recvBinary() {
|
||||
let event = await listen([this.ws, "message"]);
|
||||
if (event.data instanceof Blob) {
|
||||
return event.data;
|
||||
} else {
|
||||
throw new Error("received a text message where a binary message was expected");
|
||||
}
|
||||
}
|
||||
|
||||
#sendJson(object) {
|
||||
console.debug("sendJson", object);
|
||||
this.ws.send(JSON.stringify(object));
|
||||
}
|
||||
|
||||
|
@ -100,7 +108,7 @@ class Session extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
async join(wallId) {
|
||||
async join(wallId, userInit) {
|
||||
console.info("joining wall", wallId);
|
||||
this.wallId = wallId;
|
||||
|
||||
|
@ -123,22 +131,32 @@ class Session extends EventTarget {
|
|||
|
||||
try {
|
||||
await listen([this.ws, "open"]);
|
||||
await this.joinInner();
|
||||
await this.joinInner(wallId, userInit);
|
||||
} catch (error) {
|
||||
this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async joinInner() {
|
||||
async joinInner(wallId, userInit) {
|
||||
let version = await this.#recvJson();
|
||||
console.info("protocol version", version.version);
|
||||
// TODO: This should probably verify that the version is compatible.
|
||||
// We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.
|
||||
|
||||
let init = {
|
||||
brush: userInit.brush,
|
||||
};
|
||||
if (this.wallId == null) {
|
||||
this.#sendJson({ login: "new", user: this.userId });
|
||||
this.#sendJson({
|
||||
user: this.userId,
|
||||
init,
|
||||
});
|
||||
} else {
|
||||
this.#sendJson({ login: "join", user: this.userId, wall: this.wallId });
|
||||
this.#sendJson({
|
||||
user: this.userId,
|
||||
wall: wallId,
|
||||
init,
|
||||
});
|
||||
}
|
||||
|
||||
let loginResponse = await this.#recvJson();
|
||||
|
@ -164,9 +182,9 @@ class Session extends EventTarget {
|
|||
while (true) {
|
||||
let event = await listen([this.ws, "message"]);
|
||||
if (typeof event.data == "string") {
|
||||
await this.#processEvent(JSON.parse(event.data));
|
||||
await this.#processNotify(JSON.parse(event.data));
|
||||
} else {
|
||||
console.warn("binary event not yet supported");
|
||||
console.warn("unhandled binary event", event.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -174,27 +192,74 @@ class Session extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
async #processEvent(event) {
|
||||
if (event.kind != null) {
|
||||
async #processNotify(notify) {
|
||||
if (notify.notify == "wall") {
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event("action"), {
|
||||
sessionId: event.sessionId,
|
||||
kind: event.kind,
|
||||
Object.assign(new Event("wallEvent"), {
|
||||
sessionId: notify.sessionId,
|
||||
wallEvent: notify.wallEvent,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (notify.notify == "chunks") {
|
||||
let chunkData = await this.#recvBinary();
|
||||
this.dispatchEvent(
|
||||
Object.assign(new Event("chunks"), {
|
||||
chunkInfo: notify.chunks,
|
||||
chunkData,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async reportCursor(x, y) {
|
||||
sendCursor(x, y) {
|
||||
this.#sendJson({
|
||||
event: "cursor",
|
||||
position: { x, y },
|
||||
request: "wall",
|
||||
wallEvent: {
|
||||
event: "cursor",
|
||||
position: { x, y },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
sendPlot(points) {
|
||||
this.#sendJson({
|
||||
request: "wall",
|
||||
wallEvent: {
|
||||
event: "plot",
|
||||
points,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
sendSetBrush(brush) {
|
||||
this.#sendJson({
|
||||
request: "wall",
|
||||
wallEvent: {
|
||||
event: "setBrush",
|
||||
brush,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
sendViewport({ left, top, right, bottom }) {
|
||||
this.#sendJson({
|
||||
request: "viewport",
|
||||
topLeft: { x: left, y: top },
|
||||
bottomRight: { x: right, y: bottom },
|
||||
});
|
||||
}
|
||||
|
||||
sendMoreChunks() {
|
||||
this.#sendJson({
|
||||
request: "moreChunks",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function newSession(userId, wallId) {
|
||||
export async function newSession(userId, wallId, userInit) {
|
||||
let session = new Session(userId);
|
||||
await session.join(wallId);
|
||||
await session.join(wallId, userInit);
|
||||
return session;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
import { Pixmap } from "./haku.js";
|
||||
import { OnlineUsers } from "./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");
|
||||
}
|
||||
|
||||
syncFromPixmap() {
|
||||
this.ctx.putImageData(this.pixmap.imageData, 0, 0);
|
||||
}
|
||||
|
||||
syncToPixmap() {
|
||||
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.pixmap.imageData.data.set(imageData.data, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class Wall {
|
||||
#chunks = new Map();
|
||||
onlineUsers = new OnlineUsers();
|
||||
|
||||
constructor(chunkSize) {
|
||||
this.chunkSize = chunkSize;
|
||||
constructor(wallInfo) {
|
||||
this.chunkSize = wallInfo.chunkSize;
|
||||
this.onlineUsers = new OnlineUsers(wallInfo);
|
||||
}
|
||||
|
||||
static chunkKey(x, y) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue