Compare commits

..

3 commits

Author SHA1 Message Date
0ddc42c00f sidebar layout
switch the app from floating panels to a static sidebar on the right with resizable tools
expect more layout bugs from now on
2025-06-27 23:24:09 +02:00
f78f3136d9 optimizing loading times
- reducing the waterfall for JavaScript
- making the throbber appear properly if not all CSS is loaded; fonts.css and index.css are made non render-blocking
2025-06-26 19:17:06 +02:00
b4acab2c9c fix jank when moving the mouse cursor outside the canvas
mouse events are now consistently sourced from the window, so if you try to draw and move your mouse over the panel, it won't glitch out
2025-06-26 18:54:28 +02:00
11 changed files with 287 additions and 186 deletions

View file

@ -18,6 +18,9 @@
--font-monospace: "Iosevka Hyperlegible", monospace; --font-monospace: "Iosevka Hyperlegible", monospace;
--line-height: 1.5; --line-height: 1.5;
--line-height-em: 1.5em; --line-height-em: 1.5em;
--z-resize-handle: 50;
--z-modal: 100;
} }
/* Reset */ /* Reset */
@ -92,6 +95,10 @@ button.icon {
input { input {
border: none; border: none;
border-bottom: 1px solid var(--color-panel-border); border-bottom: 1px solid var(--color-panel-border);
&:hover:not(:focus) {
background-color: var(--color-shaded-background);
}
} }
*:focus-visible { *:focus-visible {
@ -101,15 +108,24 @@ input {
/* Modal dialogs */ /* Modal dialogs */
dialog:not([open]) { dialog {
/* Weird this doesn't seem to work by default. */ &:not([open]) {
/* Default to dialogs being invisible.
Otherwise dialogs placed on the page via HTML display on top of everything. */
display: none; display: none;
} }
dialog::backdrop { &::backdrop {
background-color: var(--dialog-backdrop); background-color: var(--dialog-backdrop);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
}
dialog:not([open]) {
}
dialog::backdrop {
}
/* Details */ /* Details */
@ -217,8 +233,6 @@ abbr {
.icon { .icon {
vertical-align: middle; vertical-align: middle;
width: 16px;
height: 16px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;

View file

@ -132,8 +132,6 @@ export class BrushBox extends HTMLElement {
connectedCallback() { connectedCallback() {
this.saveData.attachToElement(this); this.saveData.attachToElement(this);
this.classList.add("rkgk-panel");
this.brushes = []; this.brushes = [];
this.brushesContainer = this.appendChild(document.createElement("div")); this.brushesContainer = this.appendChild(document.createElement("div"));

View file

@ -1,6 +1,9 @@
import { CodeEditor, Selection } from "rkgk/code-editor.js"; import { CodeEditor, Selection } from "rkgk/code-editor.js";
import { SaveData } from "rkgk/framework.js"; import { SaveData } from "rkgk/framework.js";
import { builtInPresets } from "rkgk/brush-box.js"; import { builtInPresets } from "rkgk/brush-box.js";
import { ResizeHandle } from "rkgk/resize-handle.js";
import { BrushPreview } from "rkgk/brush-preview.js";
import { BrushCostGauges } from "rkgk/brush-cost.js";
export class BrushEditor extends HTMLElement { export class BrushEditor extends HTMLElement {
saveData = new SaveData("brushEditor"); saveData = new SaveData("brushEditor");
@ -10,13 +13,14 @@ export class BrushEditor extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this.classList.add("rkgk-panel");
this.saveData.attachToElement(this); this.saveData.attachToElement(this);
const defaultBrush = builtInPresets[0]; const defaultBrush = builtInPresets[0];
this.nameEditor = this.appendChild(document.createElement("input")); this.editorContainer = this.appendChild(document.createElement("div"));
this.editorContainer.classList.add("editor");
this.nameEditor = this.editorContainer.appendChild(document.createElement("input"));
this.nameEditor.value = this.saveData.get("name", defaultBrush.name); this.nameEditor.value = this.saveData.get("name", defaultBrush.name);
this.nameEditor.classList.add("name"); this.nameEditor.classList.add("name");
this.nameEditor.addEventListener("input", () => { this.nameEditor.addEventListener("input", () => {
@ -31,7 +35,7 @@ export class BrushEditor extends HTMLElement {
); );
}); });
this.codeEditor = this.appendChild( this.codeEditor = this.editorContainer.appendChild(
new CodeEditor([ new CodeEditor([
{ {
className: "layer-syntax", className: "layer-syntax",
@ -59,11 +63,28 @@ export class BrushEditor extends HTMLElement {
); );
}); });
this.errorHeader = this.appendChild(document.createElement("h1")); this.output = document.createElement("output");
this.errorHeader.classList.add("error-header");
this.errorArea = this.appendChild(document.createElement("pre")); this.appendChild(
this.errorArea.classList.add("errors"); new ResizeHandle({
direction: "horizontal",
inverse: true,
targetElement: this,
targetProperty: "--brush-preview-height",
initSize: 192,
minSize: 64,
}),
).classList.add("always-visible");
this.appendChild(this.output);
this.ok = this.output.appendChild(document.createElement("div"));
this.ok.classList.add("ok");
this.brushPreview = this.ok.appendChild(new BrushPreview());
this.brushCostGauges = this.ok.appendChild(new BrushCostGauges());
this.errors = this.output.appendChild(document.createElement("pre"));
this.errors.classList.add("errors");
// NOTE(localStorage): Migration from old storage key. // NOTE(localStorage): Migration from old storage key.
localStorage.removeItem("rkgk.brushEditor.code"); localStorage.removeItem("rkgk.brushEditor.code");
@ -87,11 +108,19 @@ export class BrushEditor extends HTMLElement {
} }
resetErrors() { resetErrors() {
this.errorHeader.textContent = ""; this.output.dataset.state = "ok";
this.errorArea.textContent = ""; this.errors.textContent = "";
} }
renderHakuResult(phase, result) { async updatePreview(haku, { getStats }) {
let previewResult = await this.brushPreview.renderBrush(haku);
this.brushCostGauges.update(getStats());
if (previewResult.status == "error") {
this.renderHakuResult(previewResult.result);
}
}
renderHakuResult(result) {
this.resetErrors(); this.resetErrors();
this.errorSquiggles = null; this.errorSquiggles = null;
@ -102,7 +131,7 @@ export class BrushEditor extends HTMLElement {
return; return;
} }
this.errorHeader.textContent = `${phase} failed`; this.output.dataset.state = "error";
if (result.errorKind == "diagnostics") { if (result.errorKind == "diagnostics") {
this.codeEditor.rebuildLineMap(); this.codeEditor.rebuildLineMap();
@ -112,13 +141,13 @@ export class BrushEditor extends HTMLElement {
); );
this.codeEditor.renderLayer("layer-error-squiggles"); this.codeEditor.renderLayer("layer-error-squiggles");
this.errorArea.textContent = result.diagnostics this.errors.textContent = result.diagnostics
.map( .map(
(diagnostic) => `${diagnostic.start}..${diagnostic.end}: ${diagnostic.message}`, (diagnostic) => `${diagnostic.start}..${diagnostic.end}: ${diagnostic.message}`,
) )
.join("\n"); .join("\n");
} else if (result.errorKind == "plain") { } else if (result.errorKind == "plain") {
this.errorHeader.textContent = result.message; this.errors.textContent = result.message;
} else if (result.errorKind == "exception") { } else if (result.errorKind == "exception") {
let renderer = new ErrorException(result); let renderer = new ErrorException(result);
let squiggles = renderer.prepareSquiggles(); let squiggles = renderer.prepareSquiggles();
@ -144,11 +173,11 @@ export class BrushEditor extends HTMLElement {
this.codeEditor.setSelection(new Selection(span.start, span.end)); this.codeEditor.setSelection(new Selection(span.start, span.end));
}); });
this.errorArea.replaceChildren(); this.errors.replaceChildren();
this.errorArea.appendChild(renderer); this.errors.appendChild(renderer);
} else { } else {
console.warn(`unknown error kind: ${result.errorKind}`); console.warn(`unknown error kind: ${result.errorKind}`);
this.errorHeader.textContent = "(unknown error kind)"; this.errors.textContent = "(unknown error kind)";
} }
} }

View file

@ -13,16 +13,24 @@ export class BrushPreview extends HTMLElement {
this.ctx = this.canvas.getContext("2d"); this.ctx = this.canvas.getContext("2d");
this.#resizeCanvas(); this.#resizeCanvas();
if (this.width == null || this.height == null) {
new ResizeObserver(() => this.#resizeCanvas()).observe(this);
}
} }
#resizeCanvas() { #resizeCanvas() {
this.canvas.width = this.width ?? this.clientWidth; this.canvas.width = this.width ?? this.clientWidth;
this.canvas.height = this.height ?? this.clientHeight; this.canvas.height = this.height ?? this.clientHeight;
// This can happen if the element's `display: none`.
if (this.canvas.width == 0 || this.canvas.height == 0) return;
if (this.pixmap != null) { if (this.pixmap != null) {
this.pixmap.destroy(); this.pixmap.destroy();
} }
this.pixmap = new Pixmap(this.canvas.width, this.canvas.height); this.pixmap = new Pixmap(this.canvas.width, this.canvas.height);
this.dispatchEvent(new Event(".pixmapLost"));
} }
async #renderBrushInner(haku) { async #renderBrushInner(haku) {
@ -31,9 +39,9 @@ export class BrushPreview extends HTMLElement {
runDotter: async () => { runDotter: async () => {
return { return {
fromX: this.canvas.width / 2, fromX: this.canvas.width / 2,
fromY: this.canvas.width / 2, fromY: this.canvas.height / 2,
toX: this.canvas.width / 2, toX: this.canvas.width / 2,
toY: this.canvas.width / 2, toY: this.canvas.height / 2,
num: 0, num: 0,
}; };
}, },

View file

@ -451,10 +451,10 @@ class CanvasRenderer extends HTMLElement {
async #cursorReportingBehaviour() { async #cursorReportingBehaviour() {
while (true) { while (true) {
let event = await listen([this, "mousemove"]); let event = await listen([window, "mousemove"]);
let [x, y] = this.viewport.toViewportSpace( let [x, y] = this.viewport.toViewportSpace(
event.clientX - this.clientLeft, event.clientX - this.clientLeft,
event.offsetY - this.clientTop, event.clientY - this.clientTop,
this.getWindowSize(), this.getWindowSize(),
); );
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y })); this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
@ -493,10 +493,7 @@ class InteractEvent extends Event {
continueAsDotter() { continueAsDotter() {
(async () => { (async () => {
let event = await listen( let event = await listen([window, "mousemove"], [window, "mouseup"]);
[this.canvasRenderer, "mousemove"],
[this.canvasRenderer, "mouseup"],
);
if (event.type == "mousemove") { if (event.type == "mousemove") {
let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace( let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace(

View file

@ -44,6 +44,9 @@ export class CodeEditor extends HTMLElement {
this.undoHistory = []; this.undoHistory = [];
this.undoHistoryTop = 0; this.undoHistoryTop = 0;
this.textArea.addEventListener("input", () => {
this.#codeChanged();
});
this.#textAreaAutoSizingBehaviour(); this.#textAreaAutoSizingBehaviour();
this.#keyShortcutBehaviours(); this.#keyShortcutBehaviours();
@ -76,9 +79,6 @@ export class CodeEditor extends HTMLElement {
// Resizing the text area // Resizing the text area
#textAreaAutoSizingBehaviour() { #textAreaAutoSizingBehaviour() {
this.textArea.addEventListener("input", () => {
this.#codeChanged();
});
this.#resizeTextArea(); this.#resizeTextArea();
document.fonts.addEventListener("loadingdone", () => this.#resizeTextArea()); document.fonts.addEventListener("loadingdone", () => this.#resizeTextArea());
new ResizeObserver(() => this.#resizeTextArea()).observe(this.textArea); new ResizeObserver(() => this.#resizeTextArea()).observe(this.textArea);

View file

@ -65,7 +65,7 @@ export class SaveData {
} }
attachToElement(element) { attachToElement(element) {
this.elementId = element.dataset.storageId; this.elementId = element.id;
} }
getRaw(key) { getRaw(key) {

View file

@ -44,9 +44,6 @@ main {
& > .panels { & > .panels {
--right-width: 384px; /* Overridden by JavaScript */ --right-width: 384px; /* Overridden by JavaScript */
box-sizing: border-box;
padding: 16px;
display: grid; display: grid;
grid-template-columns: [left] 1fr [right-resize] auto [right] minmax( grid-template-columns: [left] 1fr [right-resize] auto [right] minmax(
0, 0,
@ -63,9 +60,9 @@ main {
& > .left { & > .left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 16px;
pointer-events: none; pointer-events: none;
& > * { & > * {
pointer-events: auto; pointer-events: auto;
} }
@ -80,8 +77,8 @@ main {
min-height: 0; min-height: 0;
display: grid; display: grid;
grid-template-rows: minmax(0, min-content); grid-template-rows: 100%;
grid-template-columns: [floating] max-content [resize] min-content [docked] auto; grid-template-columns: [resize] min-content [docked] auto;
padding-left: 16px; padding-left: 16px;
@ -92,29 +89,14 @@ main {
min-height: 0; min-height: 0;
} }
& > rkgk-resize-handle {
pointer-events: auto;
}
& > .docked {
display: flex;
flex-direction: column;
& > * {
pointer-events: auto;
}
}
& > .docked > rkgk-brush-editor {
max-height: 100%;
}
& > .floating { & > .floating {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
padding: 16px;
& > rkgk-brush-preview { & > rkgk-brush-preview {
width: 128px; width: 128px;
height: 128px; height: 128px;
@ -126,6 +108,35 @@ main {
pointer-events: auto; pointer-events: auto;
} }
} }
& > rkgk-resize-handle {
pointer-events: auto;
}
& > .docked {
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
background-color: var(--color-panel-background);
box-shadow: 0 0 0 1px var(--color-panel-border);
pointer-events: auto;
& > * {
flex-shrink: 0;
}
& > rkgk-brush-editor {
flex-grow: 1;
flex-shrink: 1;
}
& > .menu-bar {
border-bottom: 1px solid var(--color-panel-border);
}
}
} }
} }
@ -148,6 +159,7 @@ main {
& > #js-loading { & > #js-loading {
background-color: var(--color-panel-background); background-color: var(--color-panel-background);
z-index: var(--z-modal);
display: flex; display: flex;
align-items: center; align-items: center;
@ -158,47 +170,54 @@ main {
/* Resize handle */ /* Resize handle */
rkgk-resize-handle { rkgk-resize-handle {
--width: 16px; --width: 8px;
--line-width: 2px; --line: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
z-index: var(--z-resize-handle);
&.always-visible {
--line: 1px solid var(--color-panel-border);
}
&[data-direction="horizontal"] { &[data-direction="horizontal"] {
width: 100%; width: 100%;
height: var(--width); height: var(--width);
margin: calc(var(--width) / -2) 0;
flex-direction: column; flex-direction: column;
cursor: row-resize; cursor: row-resize;
& > .visual { & > .visual {
width: 100%; width: 100%;
height: var(--line-width); height: 0;
border-bottom: var(--line);
} }
} }
&[data-direction="vertical"] { &[data-direction="vertical"] {
width: var(--width); width: var(--width);
height: 100%; height: 100%;
margin: 0 calc(var(--width) / -2);
flex-direction: row; flex-direction: row;
cursor: col-resize; cursor: col-resize;
& > .visual { & > .visual {
width: var(--line-width); width: 0;
height: 100%; height: 100%;
border-left: var(--line);
} }
} }
& > .visual {
background-color: var(--color-brand-blue);
opacity: 0%;
}
&:hover > .visual, &:hover > .visual,
&.dragging > .visual { &.dragging > .visual {
opacity: 100%; --line: 2px solid var(--color-brand-blue);
} }
} }
@ -261,12 +280,15 @@ rkgk-reticle-cursor {
/* Brush box */ /* Brush box */
rkgk-brush-box.rkgk-panel { rkgk-brush-box {
--button-size: 56px; --button-size: 56px;
height: var(--height); height: var(--height);
padding: 12px; padding: 12px;
overflow-x: hidden;
overflow-y: auto;
& > .brushes { & > .brushes {
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(
@ -275,8 +297,6 @@ rkgk-brush-box.rkgk-panel {
); );
max-height: 100%; max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
& > button { & > button {
padding: 0; padding: 0;
@ -337,20 +357,20 @@ rkgk-brush-box.rkgk-panel {
/* Code editor */ /* Code editor */
rkgk-code-editor { rkgk-code-editor {
--gutter-width: 2.75em; --gutter-width: 3.5em;
--vertical-padding: 12px;
display: block; display: block;
position: relative; position: relative;
width: 100%;
padding: var(--vertical-padding) 0;
overflow: auto; overflow: auto;
& > .layer { & > .layer {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: var(--vertical-padding);
width: 100%; width: 100%;
height: 100%;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@ -468,47 +488,60 @@ rkgk-code-editor {
&:has(textarea:focus) { &:has(textarea:focus) {
outline: 1px solid var(--color-brand-blue); outline: 1px solid var(--color-brand-blue);
outline-offset: 4px; outline-offset: -1px;
} }
} }
/* Brush editor */ /* Brush editor */
rkgk-brush-editor.rkgk-panel { rkgk-brush-editor {
padding: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px;
min-height: 0;
position: relative; position: relative;
& > .editor {
display: flex;
flex-direction: column;
height: calc(100% - var(--brush-preview-height));
& > .name { & > .name {
margin-bottom: 4px; margin: 12px;
margin-bottom: 0;
font-weight: bold; font-weight: bold;
} }
& > .text-area { & > rkgk-code-editor {
display: block; height: 100%;
width: 100%; flex-shrink: 1;
margin: 0; }
resize: none;
white-space: pre-wrap;
border: none;
overflow: hidden;
box-sizing: border-box;
} }
& > .errors:empty, & > output {
& > .error-header:empty { height: 64px;
flex-grow: 1;
user-select: text;
&[data-state="ok"] > .errors {
display: none;
}
&[data-state="error"] > .ok {
display: none; display: none;
} }
& > .error-header { & > .ok {
margin: 0; display: flex;
margin-top: 0.5em; flex-direction: row;
font-size: 1rem;
color: var(--color-error); height: 100%;
& > rkgk-brush-preview {
flex-grow: 1;
}
} }
& > .errors { & > .errors {
@ -516,12 +549,11 @@ rkgk-brush-editor.rkgk-panel {
color: var(--color-error); color: var(--color-error);
white-space: pre-wrap; white-space: pre-wrap;
user-select: text;
max-height: 20em; max-height: 20em;
overflow-y: auto; overflow-y: auto;
} }
} }
}
rkgk-brush-editor-error-exception { rkgk-brush-editor-error-exception {
& > .message { & > .message {
@ -594,7 +626,6 @@ rkgk-brush-preview {
var(--checkerboard-dark) 0% 50% var(--checkerboard-dark) 0% 50%
) )
50% 50% / var(--checkerboard-size) var(--checkerboard-size); 50% 50% / var(--checkerboard-size) var(--checkerboard-size);
border-radius: 4px;
& > canvas { & > canvas {
display: block; display: block;
@ -625,26 +656,20 @@ rkgk-brush-preview {
/* Brush cost gauges */ /* Brush cost gauges */
rkgk-brush-cost-gauges, rkgk-brush-cost-gauges {
rkgk-brush-cost-gauges.rkgk-panel {
--gauge-size: 20px; --gauge-size: 20px;
height: var(--gauge-size);
border-radius: 4px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: clip; /* clip corners */
&.hidden { &.hidden {
display: none; display: none;
} }
& > rkgk-brush-cost-gauge { & > rkgk-brush-cost-gauge {
display: block; display: block;
height: var(--gauge-size); width: var(--gauge-size);
flex-grow: 1; height: 100%;
&.hidden { &.hidden {
display: none; display: none;
@ -654,7 +679,7 @@ rkgk-brush-cost-gauges.rkgk-panel {
width: 100%; width: 100%;
height: 100%; height: 100%;
clip-path: xywh(0 0 var(--progress) 100%); clip-path: inset(calc(100% - var(--progress)) 0 0 0);
background-color: var(--gauge-color); background-color: var(--gauge-color);
} }
@ -672,6 +697,10 @@ rkgk-brush-cost-gauges.rkgk-panel {
--gauge-color: #5aca40; --gauge-color: #5aca40;
} }
} }
& .icon {
background-position: 50% calc(100% - 4px);
}
} }
/* Zoom indicator */ /* Zoom indicator */
@ -789,21 +818,38 @@ rkgk-context-menu.rkgk-panel {
/* Menu bar */ /* Menu bar */
.menu-bar { .menu-bar {
--border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
width: fit-content; width: 100%;
height: 24px; height: 28px;
border-radius: var(--border-radius);
& > a { margin: 0;
padding: 0;
list-style: none;
& > li {
display: flex;
flex-direction: row;
align-items: center;
height: 100%;
}
& > li.icon {
display: block; display: block;
width: 28px;
height: 28px;
}
& > li > a {
display: flex;
flex-direction: row;
align-items: center;
color: var(--color-text); color: var(--color-text);
padding: 4px 8px; height: 100%;
padding: 0 8px;
text-decoration: none; text-decoration: none;
line-height: 1; line-height: 1;
@ -816,16 +862,6 @@ rkgk-context-menu.rkgk-panel {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
&:first-child {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
&:last-child {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
} }
& > hr { & > hr {

View file

@ -18,8 +18,6 @@ let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
let reticleRenderer = main.querySelector("rkgk-reticle-renderer"); let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
let brushBox = main.querySelector("rkgk-brush-box"); let brushBox = main.querySelector("rkgk-brush-box");
let brushEditor = main.querySelector("rkgk-brush-editor"); let brushEditor = main.querySelector("rkgk-brush-editor");
let brushPreview = main.querySelector("rkgk-brush-preview");
let brushCostGauges = main.querySelector("rkgk-brush-cost-gauges");
let zoomIndicator = main.querySelector("rkgk-zoom-indicator"); let zoomIndicator = main.querySelector("rkgk-zoom-indicator");
let welcome = main.querySelector("rkgk-welcome"); let welcome = main.querySelector("rkgk-welcome");
let connectionStatus = main.querySelector("rkgk-connection-status"); let connectionStatus = main.querySelector("rkgk-connection-status");
@ -253,7 +251,7 @@ function readUrl(urlString) {
let result = await currentUser.haku.evalBrush( let result = await currentUser.haku.evalBrush(
selfController(interactionQueue, wall, event), selfController(interactionQueue, wall, event),
); );
brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result); brushEditor.renderHakuResult(result);
}); });
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render()); canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
@ -263,26 +261,19 @@ function readUrl(urlString) {
// Brush editor // Brush editor
function updateBrushPreview() {
brushEditor.updatePreview(currentUser.haku, {
getStats: () => currentUser.getStats(session.wallInfo),
});
}
function compileBrush() { function compileBrush() {
let compileResult = currentUser.setBrush(brushEditor.code); let compileResult = currentUser.setBrush(brushEditor.code);
brushEditor.renderHakuResult("Compilation", compileResult); brushEditor.renderHakuResult(compileResult);
brushCostGauges.update(currentUser.getStats(session.wallInfo)); if (compileResult.status == "ok") {
updateBrushPreview();
if (compileResult.status != "ok") {
brushPreview.setErrorFlag();
return;
} }
brushPreview.renderBrush(currentUser.haku).then((previewResult) => {
brushCostGauges.update(currentUser.getStats(session.wallInfo));
if (previewResult.status == "error") {
brushEditor.renderHakuResult(
previewResult.phase == "eval" ? "Evaluation" : "Rendering",
previewResult.result,
);
}
});
} }
compileBrush(); compileBrush();
@ -294,6 +285,8 @@ function readUrl(urlString) {
}); });
}); });
brushEditor.brushPreview.addEventListener(".pixmapLost", updateBrushPreview);
// Brush box // Brush box
function updateBrushBoxDirtyState() { function updateBrushBoxDirtyState() {

View file

@ -3,13 +3,31 @@ import { listen, SaveData } from "rkgk/framework.js";
export class ResizeHandle extends HTMLElement { export class ResizeHandle extends HTMLElement {
saveData = new SaveData("resizeHandle"); saveData = new SaveData("resizeHandle");
constructor(props) {
super();
this.props = props;
}
connectedCallback() { connectedCallback() {
this.direction = this.getAttribute("data-direction"); let props = this.props ?? this.dataset;
this.targetId = this.getAttribute("data-target");
this.direction = this.dataset.direction = props.direction;
this.targetProperty = props.targetProperty;
this.initSize = parseInt(props.initSize);
this.minSize = parseInt(props.minSize);
this.inverse = props.inverse != null;
if (props.targetElement != null) {
// In case you want to construct the resize handle programatically:
// pass in the target element via targetElement.
// Don't forget to set its id.
this.target = props.targetElement;
this.targetId = this.target.id;
} else {
// Else use data-target.
this.targetId = props.target;
this.target = document.getElementById(this.targetId); this.target = document.getElementById(this.targetId);
this.targetProperty = this.getAttribute("data-target-property"); }
this.initSize = parseInt(this.getAttribute("data-init-size"));
this.minSize = parseInt(this.getAttribute("data-min-size"));
this.saveData.elementId = this.targetId; this.saveData.elementId = this.targetId;
@ -58,8 +76,10 @@ export class ResizeHandle extends HTMLElement {
if (event.type == "mousemove") { if (event.type == "mousemove") {
let delta = let delta =
this.direction == "vertical" this.direction == "vertical"
? mouseDown.clientX - event.clientX ? event.clientX - mouseDown.clientX
: event.clientY - mouseDown.clientY; : event.clientY - mouseDown.clientY;
if (this.inverse) delta = -delta;
this.#setSize(startingSize + delta); this.#setSize(startingSize + delta);
this.#updateTargetProperty(); this.#updateTargetProperty();
} else if (event.type == "mouseup") { } else if (event.type == "mouseup") {

View file

@ -7,8 +7,6 @@
<title>rakugaki</title> <title>rakugaki</title>
<link rel="stylesheet" href="{{ static 'base.css' }}"> <link rel="stylesheet" href="{{ static 'base.css' }}">
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
<link rel="stylesheet" href="{{ static 'index.css' }}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -27,13 +25,20 @@
import "rkgk/brush-editor.js"; import "rkgk/brush-editor.js";
import "rkgk/brush-preview.js"; import "rkgk/brush-preview.js";
import "rkgk/canvas-renderer.js"; import "rkgk/canvas-renderer.js";
import "rkgk/code-editor.js";
import "rkgk/connection-status.js"; import "rkgk/connection-status.js";
import "rkgk/context-menu.js";
import "rkgk/framework.js"; import "rkgk/framework.js";
import "rkgk/haku.js";
import "rkgk/online-users.js";
import "rkgk/painter.js";
import "rkgk/random.js";
import "rkgk/resize-handle.js"; import "rkgk/resize-handle.js";
import "rkgk/reticle-renderer.js"; import "rkgk/reticle-renderer.js";
import "rkgk/session.js"; import "rkgk/session.js";
import "rkgk/throbber.js"; import "rkgk/throbber.js";
import "rkgk/viewport.js"; import "rkgk/viewport.js";
import "rkgk/wall.js";
import "rkgk/welcome.js"; import "rkgk/welcome.js";
import "rkgk/zoom-indicator.js"; import "rkgk/zoom-indicator.js";
@ -55,44 +60,45 @@
</head> </head>
<body> <body>
<!-- We shouldn't consider these stylesheets render-blocking.
About the only render-blocking thing on the page is the throbber. -->
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
<link rel="stylesheet" href="{{ static 'index.css' }}">
<main> <main>
<rkgk-canvas-renderer class="fullscreen"></rkgk-canvas-renderer> <rkgk-canvas-renderer class="fullscreen"></rkgk-canvas-renderer>
<rkgk-reticle-renderer class="fullscreen"></rkgk-reticle-renderer> <rkgk-reticle-renderer class="fullscreen"></rkgk-reticle-renderer>
<div class="panels fullscreen" id="panels-overlay"> <div class="panels fullscreen" id="panels-overlay">
<div class="left"> <div class="left">
<div class="rkgk-panel menu-bar">
<a class="icon icon-rkgk-grayscale" title="I know this menu bar is really ugly. Sorry about that."></a>
<hr>
<a href="/docs/rkgk.html">Manual</a>
</div>
<rkgk-zoom-indicator class="rkgk-panel"></rkgk-zoom-indicator> <rkgk-zoom-indicator class="rkgk-panel"></rkgk-zoom-indicator>
</div> </div>
<div class="right"> <div class="right">
<div class="floating">
<rkgk-brush-preview></rkgk-brush-preview>
<rkgk-brush-cost-gauges class="rkgk-panel"></rkgk-brush-cost-gauges>
</div>
<rkgk-resize-handle <rkgk-resize-handle
data-direction="vertical" data-direction="vertical"
data-inverse
data-target="panels-overlay" data-target="panels-overlay"
data-target-property="--right-width" data-target-property="--right-width"
data-init-size="528" data-init-size="528"
data-min-size="384"></rkgk-resize-handle> data-min-size="256"></rkgk-resize-handle>
<div class="docked"> <div class="docked">
<rkgk-brush-box <menu class="menu-bar">
id="brush-box" <li class="icon icon-rkgk-grayscale"></li>
data-storage-id="brush-box"></rkgk-brush-box> <hr>
<li><a href="/docs/rkgk.html">Manual</a></li>
</menu>
<rkgk-brush-box id="brush-box"></rkgk-brush-box>
<rkgk-resize-handle <rkgk-resize-handle
class="always-visible"
data-direction="horizontal" data-direction="horizontal"
data-target="brush-box" data-target="brush-box"
data-target-property="--height" data-target-property="--height"
data-init-size="168" data-init-size="168"
data-min-size="96"></rkgk-resize-handle> data-min-size="96"></rkgk-resize-handle>
<rkgk-brush-editor <rkgk-brush-editor id="brush-editor"></rkgk-brush-editor>
data-storage-id="brush-editor"></rkgk-brush-editor>
</div> </div>
</div> </div>
</div> </div>