introduce tags, structs, and reticles

this was meant to be split into smaller changes, but I realised I edited my existing revision too late.
This commit is contained in:
りき萌 2024-09-08 13:53:29 +02:00
parent 8356b6c750
commit 5b7d9586ea
26 changed files with 1113 additions and 351 deletions

View file

@ -1,12 +1,12 @@
import { CodeEditor, getLineStart } from "rkgk/code-editor.js";
import { BrushPreview } from "rkgk/brush-preview.js";
import { CodeEditor } from "rkgk/code-editor.js";
const defaultBrush = `
-- This is your brush.
-- Try playing around with the numbers,
-- Try playing around with the numbers,
-- and see what happens!
stroke 8 #000 (vec 0 0)
withDotter \d ->
stroke 8 #000 (d To)
`.trim();
export class BrushEditor extends HTMLElement {

View file

@ -21,32 +21,35 @@ export class BrushPreview extends HTMLElement {
this.pixmap = new Pixmap(this.canvas.width, this.canvas.height);
}
#renderBrushInner(haku) {
haku.resetVm();
async #renderBrushInner(haku) {
this.pixmap.clear();
let evalResult = await haku.evalBrush({
runDotter: async () => {
return {
fromX: this.canvas.width / 2,
fromY: this.canvas.width / 2,
toX: this.canvas.width / 2,
toY: this.canvas.width / 2,
num: 0,
};
},
let evalResult = haku.evalBrush();
runScribble: async (renderToPixmap) => {
return renderToPixmap(this.pixmap, 0, 0);
},
});
if (evalResult.status != "ok") {
return { status: "error", phase: "eval", result: evalResult };
}
this.pixmap.clear();
let renderResult = haku.renderValue(
this.pixmap,
this.canvas.width / 2,
this.canvas.height / 2,
);
if (renderResult.status != "ok") {
return { status: "error", phase: "render", result: renderResult };
}
this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
return { status: "ok" };
}
renderBrush(haku) {
async renderBrush(haku) {
this.unsetErrorFlag();
let result = this.#renderBrushInner(haku);
let result = await this.#renderBrushInner(haku);
if (result.status == "error") {
this.setErrorFlag();
}

View file

@ -19,7 +19,7 @@ class CanvasRenderer extends HTMLElement {
this.#cursorReportingBehaviour();
this.#panningBehaviour();
this.#zoomingBehaviour();
this.#paintingBehaviour();
this.#interactionBehaviour();
this.addEventListener("contextmenu", (event) => event.preventDefault());
}
@ -388,18 +388,6 @@ class CanvasRenderer extends HTMLElement {
// Behaviours
async #cursorReportingBehaviour() {
while (true) {
let event = await listen([this, "mousemove"]);
let [x, y] = this.viewport.toViewportSpace(
event.clientX - this.clientLeft,
event.offsetY - this.clientTop,
this.getWindowSize(),
);
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
}
}
sendViewportUpdate() {
this.dispatchEvent(new Event(".viewportUpdate"));
}
@ -443,24 +431,28 @@ class CanvasRenderer extends HTMLElement {
);
}
async #paintingBehaviour() {
const paint = (x, y) => {
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize());
this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
};
async #cursorReportingBehaviour() {
while (true) {
let event = await listen([this, "mousemove"]);
let [x, y] = this.viewport.toViewportSpace(
event.clientX - this.clientLeft,
event.offsetY - this.clientTop,
this.getWindowSize(),
);
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
}
}
async #interactionBehaviour() {
while (true) {
let mouseDown = await listen([this, "mousedown"]);
if (mouseDown.button == 0) {
paint(mouseDown.offsetX, mouseDown.offsetY);
while (true) {
let event = await listen([window, "mousemove"], [window, "mouseup"]);
if (event.type == "mousemove") {
paint(event.clientX - this.clientLeft, event.offsetY - this.clientTop);
} else if (event.type == "mouseup") {
break;
}
}
let [mouseX, mouseY] = this.viewport.toViewportSpace(
mouseDown.clientX - this.clientLeft,
mouseDown.clientY - this.clientTop,
this.getWindowSize(),
);
notifyInteraction(this, "start", { mouseX, mouseY, num: 0 });
}
}
}
@ -468,6 +460,68 @@ class CanvasRenderer extends HTMLElement {
customElements.define("rkgk-canvas-renderer", CanvasRenderer);
function notifyInteraction(canvasRenderer, kind, fields) {
canvasRenderer.dispatchEvent(
Object.assign(new InteractEvent(canvasRenderer), { interactionKind: kind, ...fields }),
);
}
class InteractEvent extends Event {
constructor(canvasRenderer) {
super(".interact");
this.canvasRenderer = canvasRenderer;
}
continueAsDotter() {
(async () => {
let event = await listen(
[this.canvasRenderer, "mousemove"],
[this.canvasRenderer, "mouseup"],
);
if (event.type == "mousemove") {
let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace(
event.clientX - this.canvasRenderer.clientLeft,
event.clientY - this.canvasRenderer.clientTop,
this.canvasRenderer.getWindowSize(),
);
notifyInteraction(this.canvasRenderer, "dotter", {
previousX: this.mouseX,
previousY: this.mouseY,
mouseX,
mouseY,
num: this.num + 1,
});
}
if (event.type == "mouseup" && event.button == 0) {
// Break the loop.
return;
}
})();
if (this.previousX != null && this.previousY != null) {
return {
fromX: this.previousX,
fromY: this.previousY,
toX: this.mouseX,
toY: this.mouseY,
num: this.num,
};
} else {
return {
fromX: this.mouseX,
fromY: this.mouseY,
toX: this.mouseX,
toY: this.mouseY,
num: this.num,
};
}
}
}
class Atlas {
static getInitBuffer(chunkSize, nChunks) {
let imageSize = chunkSize * nChunks;

View file

@ -113,6 +113,12 @@ export class Pixmap {
}
}
// NOTE: This must be kept in sync with ContKind on the haku-wasm side.
export const ContKind = {
Scribble: 0,
Dotter: 1,
};
export class Haku {
#pInstance = 0;
#pBrush = 0;
@ -206,17 +212,49 @@ export class Haku {
}
}
evalBrush() {
return this.#statusCodeToResultObject(w.haku_eval_brush(this.#pInstance, this.#pBrush));
beginBrush() {
return this.#statusCodeToResultObject(w.haku_begin_brush(this.#pInstance, this.#pBrush));
}
renderValue(pixmap, translationX, translationY) {
expectedContKind() {
return w.haku_cont_kind(this.#pInstance);
}
contScribble(pixmap, translationX, translationY) {
return this.#statusCodeToResultObject(
w.haku_render_value(this.#pInstance, pixmap.ptr, translationX, translationY),
w.haku_cont_scribble(this.#pInstance, pixmap.ptr, translationX, translationY),
);
}
resetVm() {
w.haku_reset_vm(this.#pInstance);
contDotter({ fromX, fromY, toX, toY, num }) {
return this.#statusCodeToResultObject(
w.haku_cont_dotter(this.#pInstance, fromX, fromY, toX, toY, num),
);
}
async evalBrush(options) {
let { runDotter, runScribble } = options;
let result;
result = this.beginBrush();
if (result.status != "ok") return result;
while (this.expectedContKind() != ContKind.Invalid) {
switch (this.expectedContKind()) {
case ContKind.Scribble:
result = await runScribble((pixmap, translationX, translationY) => {
return this.contScribble(pixmap, translationX, translationY);
});
return result;
case ContKind.Dotter:
let dotter = await runDotter();
result = this.contDotter(dotter);
if (result.status != "ok") return result;
break;
}
}
return { status: "ok" };
}
}

View file

@ -9,6 +9,7 @@ import {
} from "rkgk/session.js";
import { debounce } from "rkgk/framework.js";
import { ReticleCursor } from "rkgk/reticle-renderer.js";
import { selfController } from "rkgk/painter.js";
const updateInterval = 1000 / 60;
@ -175,14 +176,8 @@ function readUrl(urlString) {
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);
}
if (wallEvent.kind.event == "interact") {
user.simulate(wall, wallEvent.kind.interactions);
}
}
});
@ -230,30 +225,21 @@ function readUrl(urlString) {
reportCursor(event.x, event.y);
});
let plotQueue = [];
async function flushPlotQueue() {
let points = plotQueue.splice(0, plotQueue.length);
if (points.length != 0) {
session.sendPlot(points);
let interactionQueue = [];
function flushInteractionQueue() {
if (interactionQueue.length != 0) {
session.sendInteraction(interactionQueue);
interactionQueue.splice(0);
}
}
setInterval(flushPlotQueue, updateInterval);
setInterval(flushInteractionQueue, updateInterval);
canvasRenderer.addEventListener(".paint", async (event) => {
plotQueue.push({ x: event.x, y: event.y });
if (currentUser.isBrushOk) {
brushEditor.resetErrors();
let result = currentUser.renderBrushToChunks(wall, event.x, event.y);
if (result.status == "error") {
brushEditor.renderHakuResult(
result.phase == "eval" ? "Evaluation" : "Rendering",
result.result,
);
}
}
canvasRenderer.addEventListener(".interact", async (event) => {
let result = await currentUser.haku.evalBrush(
selfController(interactionQueue, wall, event),
);
brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result);
});
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
@ -270,20 +256,23 @@ function readUrl(urlString) {
return;
}
let previewResult = brushPreview.renderBrush(currentUser.haku);
if (previewResult.status == "error") {
brushEditor.renderHakuResult(
previewResult.phase == "eval" ? "Evaluation" : "Rendering",
previewResult.result,
);
}
brushPreview.renderBrush(currentUser.haku).then((previewResult) => {
if (previewResult.status == "error") {
brushEditor.renderHakuResult(
previewResult.phase == "eval" ? "Evaluation" : "Rendering",
previewResult.result,
);
}
});
}
compileBrush();
brushEditor.addEventListener(".codeChanged", async () => {
flushPlotQueue();
compileBrush();
session.sendSetBrush(brushEditor.code);
interactionQueue.push({
kind: "setBrush",
brush: brushEditor.code,
});
});
session.eventLoop();

View file

@ -1,5 +1,5 @@
import { Haku } from "rkgk/haku.js";
import { Painter } from "rkgk/painter.js";
import { ContKind, Haku } from "rkgk/haku.js";
import { renderToChunksInArea, dotterRenderArea } from "rkgk/painter.js";
export class User {
nickname = "";
@ -7,12 +7,11 @@ export class User {
reticle = null;
isBrushOk = false;
simulation = null;
constructor(wallInfo, nickname) {
this.nickname = nickname;
this.haku = new Haku(wallInfo.hakuLimits);
this.painter = new Painter(wallInfo.paintArea);
}
destroy() {
@ -38,6 +37,58 @@ export class User {
return result;
}
simulate(wall, interactions) {
console.group("simulate");
for (let interaction of interactions) {
if (interaction.kind == "setBrush") {
this.simulation = null;
this.setBrush(interaction.brush);
}
if (this.isBrushOk) {
if (this.simulation == null) {
console.log("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(
wall,
this.simulation.renderArea,
(pixmap, translationX, translationY) => {
return this.haku.contScribble(pixmap, translationX, translationY);
},
);
console.log("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;
}
}
}
export class OnlineUsers extends EventTarget {

View file

@ -1,43 +1,69 @@
export class Painter {
constructor(paintArea) {
this.paintArea = paintArea;
}
import { listen } from "rkgk/framework.js";
renderBrushToWall(haku, centerX, centerY, wall) {
haku.resetVm();
let evalResult = haku.evalBrush();
if (evalResult.status != "ok")
return { status: "error", phase: "eval", result: evalResult };
let left = centerX - this.paintArea / 2;
let top = centerY - this.paintArea / 2;
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);
chunk.markModified();
let renderResult = haku.renderValue(chunk.pixmap, x, y);
if (renderResult.status != "ok") {
return { status: "error", phase: "render", result: renderResult };
}
}
function* chunksInRectangle(left, top, right, bottom, chunkSize) {
let leftChunk = Math.floor(left / chunkSize);
let topChunk = Math.floor(top / chunkSize);
let rightChunk = Math.ceil(right / chunkSize);
let bottomChunk = Math.ceil(bottom / chunkSize);
for (let chunkY = topChunk; chunkY < bottomChunk; ++chunkY) {
for (let chunkX = leftChunk; chunkX < rightChunk; ++chunkX) {
yield [chunkX, chunkY];
}
for (let y = topChunk; y < bottomChunk; ++y) {
for (let x = leftChunk; x < rightChunk; ++x) {
let chunk = wall.getChunk(x, y);
chunk.syncFromPixmap();
}
}
return { status: "ok" };
}
}
export function renderToChunksInArea(wall, renderArea, renderToPixmap) {
for (let [chunkX, chunkY] of chunksInRectangle(
renderArea.left,
renderArea.top,
renderArea.right,
renderArea.bottom,
wall.chunkSize,
)) {
let chunk = wall.getOrCreateChunk(chunkX, chunkY);
let translationX = -chunkX * wall.chunkSize;
let translationY = -chunkY * wall.chunkSize;
let result = renderToPixmap(chunk.pixmap, translationX, translationY);
chunk.markModified();
if (result.status != "ok") return result;
}
return { status: "ok" };
}
export function dotterRenderArea(wall, dotter) {
let halfPaintArea = wall.paintArea / 2;
return {
left: dotter.toX - halfPaintArea,
top: dotter.toY - halfPaintArea,
right: dotter.toX + halfPaintArea,
bottom: dotter.toY + halfPaintArea,
};
}
export function selfController(interactionQueue, wall, event) {
let renderArea = null;
return {
async runScribble(renderToPixmap) {
interactionQueue.push({ kind: "scribble" });
if (renderArea != null) {
return renderToChunksInArea(wall, renderArea, renderToPixmap);
} else {
console.debug("render area is empty, nothing will be rendered");
}
return { status: "ok" };
},
async runDotter() {
let dotter = await event.continueAsDotter();
interactionQueue.push({
kind: "dotter",
from: { x: dotter.fromX, y: dotter.fromY },
to: { x: dotter.toX, y: dotter.toY },
num: dotter.num,
});
renderArea = dotterRenderArea(wall, dotter);
return dotter;
},
};
}

View file

@ -267,22 +267,13 @@ class Session extends EventTarget {
});
}
sendPlot(points) {
sendInteraction(interactions) {
console.log(interactions);
this.#sendJson({
request: "wall",
wallEvent: {
event: "plot",
points,
},
});
}
sendSetBrush(brush) {
this.#sendJson({
request: "wall",
wallEvent: {
event: "setBrush",
brush,
event: "interact",
interactions,
},
});
}

0
static/signal.js Normal file
View file

View file

@ -28,6 +28,7 @@ export class Wall {
constructor(wallInfo) {
this.chunkSize = wallInfo.chunkSize;
this.paintArea = wallInfo.paintArea;
this.onlineUsers = new OnlineUsers(wallInfo);
}