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:
parent
8356b6c750
commit
5b7d9586ea
26 changed files with 1113 additions and 351 deletions
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
0
static/signal.js
Normal file
|
@ -28,6 +28,7 @@ export class Wall {
|
|||
|
||||
constructor(wallInfo) {
|
||||
this.chunkSize = wallInfo.chunkSize;
|
||||
this.paintArea = wallInfo.paintArea;
|
||||
this.onlineUsers = new OnlineUsers(wallInfo);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue