From 37520f34f75d38241968d595d694ee20032e836f Mon Sep 17 00:00:00 2001 From: liquidev Date: Sun, 8 Sep 2024 12:09:14 +0200 Subject: [PATCH] add brush preview --- static/base.css | 4 ++ static/brush-editor.js | 1 + static/brush-preview.js | 57 +++++++++++++++++++++++++ static/index.css | 93 +++++++++++++++++++++++++++++++++-------- static/index.js | 20 ++++++++- static/resize-handle.js | 1 + template/index.hbs.html | 13 ++++-- 7 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 static/brush-preview.js diff --git a/static/base.css b/static/base.css index 5ae5a4b..bbdae71 100644 --- a/static/base.css +++ b/static/base.css @@ -26,6 +26,10 @@ body { line-height: var(--line-height); } +* { + box-sizing: border-box; +} + /* Fonts */ :root { diff --git a/static/brush-editor.js b/static/brush-editor.js index 2b36258..07c115a 100644 --- a/static/brush-editor.js +++ b/static/brush-editor.js @@ -1,4 +1,5 @@ import { CodeEditor, getLineStart } from "rkgk/code-editor.js"; +import { BrushPreview } from "rkgk/brush-preview.js"; const defaultBrush = ` -- This is your brush. diff --git a/static/brush-preview.js b/static/brush-preview.js new file mode 100644 index 0000000..46624ef --- /dev/null +++ b/static/brush-preview.js @@ -0,0 +1,57 @@ +import { Pixmap } from "rkgk/haku.js"; + +export class BrushPreview extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.canvas = this.appendChild(document.createElement("canvas")); + this.ctx = this.canvas.getContext("2d"); + this.#resizeCanvas(); + } + + #resizeCanvas() { + this.canvas.width = this.clientWidth; + this.canvas.height = this.clientHeight; + + if (this.pixmap != null) { + this.pixmap.destroy(); + } + this.pixmap = new Pixmap(this.canvas.width, this.canvas.height); + } + + #renderBrushInner(haku) { + haku.resetVm(); + + let evalResult = haku.evalBrush(); + 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) { + this.classList.remove("error"); + let result = this.#renderBrushInner(haku); + if (result.status == "error") { + this.classList.add("error"); + } + return result; + } +} + +customElements.define("rkgk-brush-preview", BrushPreview); diff --git a/static/index.css b/static/index.css index 0817b26..1b67091 100644 --- a/static/index.css +++ b/static/index.css @@ -32,7 +32,7 @@ main { padding: 16px; display: grid; - grid-template-columns: [left] 1fr [right-resize] auto [right] var(--right-width); + grid-template-columns: [left] 1fr [right-resize] auto [right] minmax(0, var(--right-width)); /* Pass all events through. Children may receive events as normal. */ pointer-events: none; @@ -43,23 +43,34 @@ main { &>.right { grid-column: right / right; - height: fit-content; min-height: 0; - max-height: 100%; - display: flex; - flex-direction: row; - justify-content: stretch; + display: grid; + grid-template-rows: minmax(0, min-content); + grid-template-columns: [floating] max-content [resize] min-content [docked] auto; - &>rkgk-resize-handle { - flex-shrink: 0; - height: auto; + padding-left: 16px; + + pointer-events: none; + + &>* { + min-width: 0; + min-height: 0; } - &>rkgk-brush-editor { - height: auto; - overflow: auto; - flex-grow: 1; + &>rkgk-resize-handle { + pointer-events: auto; + } + + &>.docked>rkgk-brush-editor { + max-height: 100%; + pointer-events: auto; + } + + &>.floating>rkgk-brush-preview { + width: 128px; + height: 128px; + pointer-events: auto; } } } @@ -175,6 +186,8 @@ rkgk-code-editor { position: relative; width: 100%; + overflow: auto; + &>.layer { position: absolute; left: 0; @@ -259,13 +272,28 @@ rkgk-code-editor { resize: none; white-space: pre-wrap; border: none; + + &:focus { + outline: none; + } + } + + &:has(textarea:focus) { + outline: 1px solid var(--color-brand-blue); + outline-offset: 4px; } } /* Brush editor */ -rkgk-brush-editor { +rkgk-brush-editor.rkgk-panel { padding: 12px; + + display: flex; + flex-direction: column; + gap: 4px; + + position: relative; &>.text-area { display: block; @@ -283,8 +311,8 @@ rkgk-brush-editor { } &>.error-header { - margin-top: 1em; - margin-bottom: 0; + margin: 0; + margin-top: 0.5em; font-size: 1rem; color: var(--color-error); } @@ -296,6 +324,36 @@ rkgk-brush-editor { } } +/* Brush preview */ + +rkgk-brush-preview { + --checkerboard-light: #f2f2f2; + --checkerboard-dark: #e1e1e1; + --checkerboard-size: 64px; + + display: block; + position: relative; + + background: + repeating-conic-gradient(var(--checkerboard-light) 0% 25%, var(--checkerboard-dark) 0% 50%) + 50% 50% / var(--checkerboard-size) var(--checkerboard-size); + border-radius: 4px; + + &.error { + &>canvas { + display: none; + } + + &::before { + content: "(error)"; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + } +} + /* Welcome screen */ rkgk-welcome { @@ -359,7 +417,8 @@ rkgk-connection-status { } &.icon { - padding: 4px 4px; + width: 24px; + height: 24px; } &:first-child { diff --git a/static/index.js b/static/index.js index f4c3c56..19cc3bb 100644 --- a/static/index.js +++ b/static/index.js @@ -16,6 +16,7 @@ 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 brushPreview = main.querySelector("rkgk-brush-preview"); let welcome = main.querySelector("rkgk-welcome"); let connectionStatus = main.querySelector("rkgk-connection-status"); @@ -260,10 +261,25 @@ function readUrl(urlString) { updateUrl(session, canvasRenderer.viewport), ); - brushEditor.renderHakuResult("Compilation", currentUser.setBrush(brushEditor.code)); + function compileBrush() { + let compileResult = currentUser.setBrush(brushEditor.code); + brushEditor.renderHakuResult("Compilation", compileResult); + + if (compileResult.status != "ok") return; + + let previewResult = brushPreview.renderBrush(currentUser.haku); + if (previewResult.status == "error") { + brushEditor.renderHakuResult( + previewResult.phase == "eval" ? "Evaluation" : "Rendering", + previewResult.result, + ); + } + } + + compileBrush(); brushEditor.addEventListener(".codeChanged", async () => { flushPlotQueue(); - brushEditor.renderHakuResult("Compilation", currentUser.setBrush(brushEditor.code)); + compileBrush(); session.sendSetBrush(brushEditor.code); }); diff --git a/static/resize-handle.js b/static/resize-handle.js index 729bb13..f8f8cac 100644 --- a/static/resize-handle.js +++ b/static/resize-handle.js @@ -47,6 +47,7 @@ export class ResizeHandle extends HTMLElement { let mouseDown = await listen([this, "mousedown"]); let startingSize = this.size; if (mouseDown.button == 0) { + mouseDown.preventDefault(); this.classList.add("dragging"); while (true) { diff --git a/template/index.hbs.html b/template/index.hbs.html index 03fd22c..4aeff19 100644 --- a/template/index.hbs.html +++ b/template/index.hbs.html @@ -22,6 +22,7 @@ import "rkgk/live-reload.js"; import "rkgk/brush-editor.js"; + import "rkgk/brush-preview.js"; import "rkgk/canvas-renderer.js"; import "rkgk/connection-status.js"; import "rkgk/framework.js"; @@ -59,14 +60,20 @@
Manual +
+
+ +
- + data-init-size="512" + data-min-size="384"> +
+ +