brush picker!

This commit is contained in:
りき萌 2025-06-19 13:48:07 +02:00
parent 9b82b211b4
commit c1612b2a94
12 changed files with 849 additions and 45 deletions

View file

@ -9,6 +9,7 @@
--color-panel-border: rgba(0, 0, 0, 20%);
--color-panel-background: #fff;
--color-shaded-background: rgba(0, 0, 0, 5%);
--color-active-background: rgba(0, 0, 0, 10%);
--dialog-backdrop: rgba(255, 255, 255, 0.5);
@ -53,10 +54,28 @@ textarea {
/* Buttons */
button {
color: var(--color-text);
border: 1px solid var(--color-panel-border);
border-radius: 9999px;
padding: 0.5rem 1.5rem;
background-color: var(--color-panel-background);
background: none;
&:hover:not(:disabled) {
background-color: var(--color-shaded-background);
}
&:active:not(:disabled) {
background-color: var(--color-active-background);
}
&:disabled {
opacity: 50%;
}
}
button.destructive {
color: var(--color-error);
}
button.icon {

348
static/brush-box.js Normal file
View file

@ -0,0 +1,348 @@
import { BrushPreview } from "rkgk/brush-preview.js";
import { Haku } from "rkgk/haku.js";
import { randomId } from "rkgk/random.js";
import { SaveData } from "rkgk/framework.js";
import { ContextMenu, globalContextMenuSpace } from "rkgk/context-menu.js";
export const builtInPresets = [
{
id: "builtin/default",
name: "Default",
code: `
-- Try playing around with the values
-- and see what happens!
color = #000
thickness = 8
withDotter \\d ->
stroke thickness color (line (d From) (d To))
`.trim(),
},
{
id: "builtin/thick",
name: "Thick",
code: `
color = #000
thickness = 48
withDotter \\d ->
stroke thickness color (line (d From) (d To))
`.trim(),
},
{
id: "builtin/pencil",
name: "Pencil",
code: `
color = #0003
thickness = 6
withDotter \\d ->
stroke thickness color (line (d From) (d To))
`.trim(),
},
{
id: "builtin/woobly",
name: "Woobly",
code: `
color = #000
minThickness = 8
maxThickness = 20
wavelength = 1
withDotter \\d ->
let pi = 3.14159265
let a = (sin (d Num * wavelength / pi) + 1) / 2
let range = maxThickness - minThickness
let thickness = minThickness + a * range
stroke thickness color (line (d From) (d To))
`.trim(),
},
{
id: "builtin/wavy",
name: "Wavy",
code: `
color = #000
thickness = 4
amplitude = 50
wavelength = 1
mag = \\v ->
hypot (vecX v) (vecY v)
norm = \\u ->
let l = mag u
u / vec l l
perpClockwise = \\v ->
vec (vecY v) (-(vecX v))
withDotter \\d ->
let pi = 3.14159265
let a = sin (d Num * wavelength / pi) * amplitude
let direction = (d To) - (d From)
let clockwise = norm (perpClockwise direction) * vec a a
let from = d From + clockwise
let to = d To + clockwise
stroke thickness color (line from to)
`.trim(),
},
{
id: "builtin/rainbow",
name: "Rainbow",
code: `
wavelength = 0.1
thickness = 8
colorCurve = \\n ->
abs (cos n)
withDotter \\d ->
let pi = 3.14159265
let l = wavelength
let r = colorCurve (d Num * l)
let g = colorCurve (d Num * l + pi/3)
let b = colorCurve (d Num * l + 2*pi/3)
let color = rgba r g b 1
stroke thickness color (line (d From) (d To))
`.trim(),
},
// ...feel free to add more presets here!
];
function presetButton(info) {
let button = document.createElement("button");
let preview = button.appendChild(new BrushPreview(56, 56));
let label = button.appendChild(document.createElement("p"));
label.classList.add("label");
label.innerText = info.name;
return { button, preview };
}
export class BrushBox extends HTMLElement {
dirty = false;
saveData = new SaveData("brushBox");
connectedCallback() {
this.saveData.attachToElement(this);
this.classList.add("rkgk-panel");
this.brushes = [];
this.brushesContainer = this.appendChild(document.createElement("div"));
this.brushesContainer.classList.add("brushes");
this.userPresets = this.saveData.get("userPresets", []);
this.updateBrushes();
// NOTE: Restoring the currently selected brush from local storage must NOT end up in a
// code editor update, so as not to reset the user's currently WIP brush.
let savedCurrentBrush = this.saveData.get("currentBrush", "builtin/default");
if (this.findBrushPreset(savedCurrentBrush)) {
this.setCurrentBrush(savedCurrentBrush);
} else {
console.warn("brush preset does not exist", savedCurrentBrush);
this.setCurrentBrush("builtin/default");
this.#sendBrushChange(this.findBrushPreset(this.currentPresetId));
}
}
initialize(wallLimits) {
this.haku = new Haku(wallLimits);
this.renderBrushes();
}
updateBrushes() {
this.brushesContainer.replaceChildren();
this.brushes = [];
const addBrush = (preset, { mutable }) => {
let pb = presetButton(preset);
this.brushesContainer.appendChild(pb.button);
this.brushes.push({
preset,
presetButton: pb,
});
pb.button.addEventListener("click", (event) => {
if (this.dirty) {
let confirmation = new ContextMenu();
confirmation.addHtmlP(
"<strong>Your brush has unsaved changes.</strong>" +
"<br>Loading another brush will discard them." +
"<br>Are you sure?",
);
confirmation.addSeparator();
confirmation.addButton("Yes").addEventListener("click", () => {
this.#sendBrushChange(preset);
});
confirmation.addHtmlP("Click anywhere else to cancel", {
classList: ["small"],
});
globalContextMenuSpace.openAtCursor(event, confirmation);
} else {
this.#sendBrushChange(preset);
}
});
pb.button.addEventListener("contextmenu", (event) => {
event.preventDefault();
let menu = new ContextMenu();
menu.addButton(this.currentPresetId == preset.id ? "Save" : "Overwrite", {
disabled: !mutable,
tooltip: mutable ? "" : "This is a built-in brush and cannot be modified",
}).addEventListener("click", () => {
this.dispatchEvent(
Object.assign(new Event(".overwritePreset"), {
preset,
}),
);
this.updateBrushes();
this.renderBrushes();
this.markClean();
this.saveUserPresets();
});
menu.addButton("Duplicate").addEventListener("click", () => {
let id = this.saveUserPreset(preset);
this.updateBrushes();
this.renderBrushes();
if (!this.dirty) {
this.currentPresetId = id;
let duped = this.findBrushPreset(this.currentPresetId);
this.#sendBrushChange(duped);
}
});
menu.addButton("Delete", {
classList: ["destructive"],
disabled: !mutable,
tooltip: mutable ? "" : "This is a built-in brush and cannot be deleted",
}).addEventListener("click", () => {
// NOTE: This leaves the box in a weird state where there is no current brush.
// That's okay; code should not assume there's a current preset ID.
if (this.currentPresetId == preset.id) {
this.currentPresetId = null;
this.markDirty();
}
let index = this.userPresets.indexOf(preset);
this.userPresets.splice(index, 1);
this.updateBrushes();
this.renderBrushes();
this.saveUserPresets();
// NOTE: No brush change because we don't want to overwrite the editor.
});
globalContextMenuSpace.openAtCursor(event, menu);
});
};
for (let preset of builtInPresets) addBrush(preset, { mutable: false });
for (let preset of this.userPresets) addBrush(preset, { mutable: true });
let saveButton = this.brushesContainer.appendChild(document.createElement("button"));
saveButton.classList.add("save-button");
let plusIcon = saveButton.appendChild(document.createElement("div"));
plusIcon.classList.add("icon", "icon-plus");
let saveLabel = saveButton.appendChild(document.createElement("p"));
saveLabel.classList.add("label");
saveLabel.innerText = "Save new";
saveButton.addEventListener("click", () => {
this.dispatchEvent(new Event(".clickNew"));
});
if (this.currentPresetId != null) this.setCurrentBrush(this.currentPresetId);
}
async renderBrushes() {
for (let brush of this.brushes) {
this.haku.setBrush(brush.preset.code);
await brush.presetButton.preview.renderBrush(this.haku);
}
}
setCurrentBrush(id) {
this.currentPresetId = id;
this.saveData.set("currentBrush", this.currentPresetId);
for (let brush of this.brushes) {
let button = brush.presetButton.button;
if (brush.preset.id != id) button.classList.remove("current");
else button.classList.add("current");
}
}
indexOfBrushPreset(id) {
for (let i in this.brushes) {
if (this.brushes[i].preset.id == id) return i;
}
return null;
}
findBrushPreset(id) {
let index = this.indexOfBrushPreset(id);
if (index == null) return null;
return this.brushes[index].preset;
}
markDirty() {
this.dirty = true;
this.classList.add("unsaved");
}
markClean() {
this.dirty = false;
this.classList.remove("unsaved");
}
saveUserPreset({ name, code }, id = null) {
if (id == null) {
// Save a new preset.
id = `user/${randomId()}`;
console.log("saving new brush", id);
this.userPresets.push({
id,
name,
code,
});
} else {
// Overwrite an existing one.
let preset = this.userPresets.find((p) => p.id == id);
console.log("overwriting existing brush", preset);
preset.code = code;
}
this.saveUserPresets();
return id;
}
saveUserPresets() {
this.saveData.set("userPresets", this.userPresets);
}
#sendBrushChange(preset) {
this.dispatchEvent(
Object.assign(new Event(".brushChange"), {
preset,
}),
);
}
}
customElements.define("rkgk-brush-box", BrushBox);

View file

@ -1,15 +1,10 @@
import { CodeEditor } from "rkgk/code-editor.js";
const defaultBrush = `
-- This is your brush.
-- Try playing around with the numbers,
-- and see what happens!
withDotter \\d ->
stroke 8 #000 (d To)
`.trim();
import { SaveData } from "rkgk/framework.js";
import { builtInPresets } from "rkgk/brush-box.js";
export class BrushEditor extends HTMLElement {
saveData = new SaveData("brushEditor");
constructor() {
super();
}
@ -17,6 +12,25 @@ export class BrushEditor extends HTMLElement {
connectedCallback() {
this.classList.add("rkgk-panel");
this.saveData.attachToElement(this);
const defaultBrush = builtInPresets[0];
this.nameEditor = this.appendChild(document.createElement("input"));
this.nameEditor.value = this.saveData.get("name", defaultBrush.name);
this.nameEditor.classList.add("name");
this.nameEditor.addEventListener("input", () => {
this.saveData.set("name", this.nameEditor.value);
this.dispatchEvent(
Object.assign(
new Event(".nameChanged", {
newName: this.nameEditor.value,
}),
),
);
});
this.codeEditor = this.appendChild(
new CodeEditor([
{
@ -25,9 +39,14 @@ export class BrushEditor extends HTMLElement {
},
]),
);
this.codeEditor.setCode(localStorage.getItem("rkgk.brushEditor.code") ?? defaultBrush);
this.codeEditor.setCode(
// NOTE(localStorage): Migration from old storage key.
this.saveData.get("code") ??
localStorage.getItem("rkgk.brushEditor.code") ??
defaultBrush.code,
);
this.codeEditor.addEventListener(".codeChanged", (event) => {
localStorage.setItem("rkgk.brushEditor.code", event.newCode);
this.saveData.set("code", event.newCode);
this.dispatchEvent(
Object.assign(new Event(".codeChanged"), {
@ -41,12 +60,28 @@ export class BrushEditor extends HTMLElement {
this.errorArea = this.appendChild(document.createElement("pre"));
this.errorArea.classList.add("errors");
// NOTE(localStorage): Migration from old storage key.
localStorage.removeItem("rkgk.brushEditor.code");
}
get name() {
return this.nameEditor.value;
}
setName(newName) {
this.nameEditor.value = newName;
this.saveData.set("name", this.nameEditor.value);
}
get code() {
return this.codeEditor.code;
}
setCode(newCode) {
this.codeEditor.setCode(newCode);
}
resetErrors() {
this.errorHeader.textContent = "";
this.errorArea.textContent = "";

View file

@ -1,19 +1,23 @@
import { Pixmap } from "rkgk/haku.js";
export class BrushPreview extends HTMLElement {
constructor() {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
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;
this.canvas.width = this.width ?? this.clientWidth;
this.canvas.height = this.height ?? this.clientHeight;
if (this.pixmap != null) {
this.pixmap.destroy();
@ -51,6 +55,7 @@ export class BrushPreview extends HTMLElement {
this.unsetErrorFlag();
let result = await this.#renderBrushInner(haku);
if (result.status == "error") {
console.error(result);
this.setErrorFlag();
}
return result;

View file

@ -54,6 +54,10 @@ export class CodeEditor extends HTMLElement {
return this.textArea.value;
}
set code(newCode) {
this.textArea.value = newCode;
}
#codeChanged() {
this.#resizeTextArea();
this.#renderLayers();

115
static/context-menu.js Normal file
View file

@ -0,0 +1,115 @@
// - Create your context menu by instantiating ContextMenu and adding elements to it
// - Open your menu in response to a contextmenu event by using globalContextMenuSpace.openAtCursor(event, menu)
import { listen } from "rkgk/framework.js";
export class ContextMenu extends HTMLElement {
connectedCallback() {
this.classList.add("rkgk-panel");
this.closeController = new AbortController();
this.hasMouse = false;
this.addEventListener("mouseover", () => (this.hasMouse = true));
this.addEventListener("mouseleave", () => (this.hasMouse = false));
window.addEventListener(
"mousedown",
() => {
if (!this.hasMouse) {
this.close();
}
},
{ signal: this.closeController.signal },
);
window.addEventListener(
"keydown",
(event) => {
if (event.key == "Escape") {
this.close();
}
},
{ signal: this.closeController.signal },
);
}
disconnectedCallback() {
this.closeController.abort();
}
close() {
this.space.close(this);
}
addButton(label, options) {
let { classList, disabled, tooltip, noCloseOnClick } = options ?? {};
classList ??= [];
disabled ??= false;
tooltip ??= "";
noCloseOnClick ??= false;
let button = this.appendChild(document.createElement("button"));
button.classList.add(...classList);
button.innerText = label;
button.disabled = disabled;
button.title = tooltip;
button.addEventListener("click", () => {
if (!noCloseOnClick) this.close();
});
return button;
}
addHtmlP(text, options) {
let { classList } = options ?? {};
classList ??= [];
let p = this.appendChild(document.createElement("p"));
p.innerHTML = text;
p.classList.add(...classList);
return p;
}
addSeparator() {
this.appendChild(document.createElement("hr"));
}
}
customElements.define("rkgk-context-menu", ContextMenu);
export class ContextMenuSpace extends HTMLElement {
open(contextMenu) {
contextMenu.space = this;
this.appendChild(contextMenu);
}
close(contextMenu) {
this.removeChild(contextMenu);
}
openAtCursor(cursorEvent, contextMenu) {
this.open(contextMenu);
// NOTE: This assumes the context menu space is positioned at (0, 0) and spans the full page.
let areaWidth = this.clientWidth;
let areaHeight = this.clientHeight;
let menuWidth = contextMenu.clientWidth;
let menuHeight = contextMenu.clientHeight;
let x = cursorEvent.clientX;
let y = cursorEvent.clientY;
// If the menu would reach outside the screen bounds, snap it back in.
if (x + menuWidth > areaWidth) x -= menuWidth;
if (y + menuHeight > areaHeight) y -= menuHeight;
contextMenu.style.transform = `translate(${x}px, ${y}px)`;
}
}
customElements.define("rkgk-context-menu-space", ContextMenuSpace);
export const globalContextMenuSpace = document.querySelector("rkgk-context-menu-space");

View file

@ -28,3 +28,43 @@ export function debounce(time, fn) {
}
};
}
export class SaveData {
constructor(prefix) {
this.prefix = `rkgk.${prefix}`;
this.elementId = "<global>";
}
#localStorageKey(key) {
return `${this.prefix}.${this.elementId}.${key}`;
}
attachToElement(element) {
this.elementId = element.dataset.storageId;
}
getRaw(key) {
return localStorage.getItem(this.#localStorageKey(key));
}
setRaw(key, value) {
localStorage.setItem(this.#localStorageKey(key), value);
}
get(key, defaultValue) {
let value = this.getRaw(key);
if (value == null) {
return defaultValue;
} else {
try {
return JSON.parse(value);
} catch (e) {
throw new Error(`${this.#localStorageKey(key)}`, { cause: e });
}
}
}
set(key, value) {
this.setRaw(key, JSON.stringify(value));
}
}

View file

@ -1,6 +1,12 @@
/* index.css - styles for index.html and generally main parts of the app
For shared styles (such as color definitions) check out base.css. */
* {
/* On the main page, we don't really want to permit selecting things.
It comes off as janky-looking. */
user-select: none;
}
/* Main container layout */
html {
@ -90,9 +96,17 @@ main {
pointer-events: auto;
}
& > .docked {
display: flex;
flex-direction: column;
& > * {
pointer-events: auto;
}
}
& > .docked > rkgk-brush-editor {
max-height: 100%;
pointer-events: auto;
}
& > .floating {
@ -144,25 +158,47 @@ main {
/* Resize handle */
rkgk-resize-handle {
--width: 16px;
--line-width: 2px;
display: flex;
justify-content: center;
flex-shrink: 0;
&[data-direction="horizontal"] {
width: 100%;
height: var(--width);
flex-direction: column;
cursor: row-resize;
& > .visual {
width: 100%;
height: var(--line-width);
}
}
&[data-direction="vertical"] {
display: block;
width: 16px;
width: var(--width);
height: 100%;
flex-direction: row;
cursor: col-resize;
& > .visual {
width: 2px;
width: var(--line-width);
height: 100%;
background-color: var(--color-brand-blue);
margin: 0 auto;
opacity: 0%;
}
}
&:hover > .visual,
&.dragging > .visual {
opacity: 100%;
}
& > .visual {
background-color: var(--color-brand-blue);
opacity: 0%;
}
&:hover > .visual,
&.dragging > .visual {
opacity: 100%;
}
}
@ -223,6 +259,81 @@ rkgk-reticle-cursor {
}
}
/* Brush box */
rkgk-brush-box.rkgk-panel {
--button-size: 56px;
height: var(--height);
padding: 12px;
& > .brushes {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(var(--button-size), 1fr)
);
max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
& > button {
padding: 0;
border: 1px solid transparent;
border-radius: 4px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
white-space: nowrap;
&.current {
border-color: var(--color-brand-blue);
}
& > rkgk-brush-preview {
width: var(--button-size);
aspect-ratio: 1 / 1;
background: none;
}
& > .label {
margin: 0;
font-size: 80%;
}
&.save-button {
display: none; /* visible when current brush is modified */
& > .icon {
width: var(--button-size);
height: var(--button-size);
}
& > .icon,
& > .label {
opacity: 35%;
}
}
}
}
&.unsaved > .brushes {
& > .current > .label::after {
content: "*";
}
& > .save-button {
display: flex;
}
}
}
/* Code editor */
rkgk-code-editor {
@ -258,8 +369,6 @@ rkgk-code-editor {
}
& > .layer-gutter {
user-select: none;
counter-reset: line;
color: transparent;
@ -318,10 +427,18 @@ rkgk-code-editor {
resize: none;
white-space: pre-wrap;
border: none;
background: none;
&:focus {
/* The outline is displayed on the parent element to also surround the gutter. */
outline: none;
}
&::selection {
/* The selection color has to be overridden for a good contrast ratio between text and
the selection. */
background-color: rgba(from var(--color-brand-blue) r g b / 0.3);
}
}
&:has(textarea:focus) {
@ -341,6 +458,11 @@ rkgk-brush-editor.rkgk-panel {
position: relative;
& > .name {
margin-bottom: 4px;
font-weight: bold;
}
& > .text-area {
display: block;
width: 100%;
@ -389,8 +511,12 @@ rkgk-brush-preview {
border-radius: 4px;
& > canvas {
display: block;
border-radius: 4px;
max-width: 100%;
height: auto;
/* The brush preview doesn't scale with DPI as easily as the canvas renderer does,
so instead we pixelate it. */
image-rendering: pixelated;
@ -487,8 +613,6 @@ rkgk-zoom-indicator.rkgk-panel {
line-height: 1;
width: 6ch;
user-select: none;
font-variant-numeric: tabular-nums;
text-align: center;
}
@ -531,6 +655,51 @@ rkgk-connection-status {
}
}
/* Context menu */
rkgk-context-menu-space {
pointer-events: none;
& > rkgk-context-menu {
pointer-events: all;
}
}
rkgk-context-menu.rkgk-panel {
width: max-content;
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: clip;
& > p {
margin: 0;
padding: 6px 12px;
&.small {
padding: 4px 12px;
font-size: 80%;
opacity: 75%;
}
}
& > button {
padding: 6px 12px;
border: none;
border-radius: 0;
text-align: left;
}
& > hr {
margin: 0;
width: 100%;
border-bottom: 1px solid var(--color-panel-border);
}
}
/* Menu bar */
.menu-bar {

View file

@ -16,6 +16,7 @@ const updateInterval = 1000 / 60;
let main = document.querySelector("main");
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
let brushBox = main.querySelector("rkgk-brush-box");
let brushEditor = main.querySelector("rkgk-brush-editor");
let brushPreview = main.querySelector("rkgk-brush-preview");
let brushCostGauges = main.querySelector("rkgk-brush-cost-gauges");
@ -134,6 +135,7 @@ function readUrl(urlString) {
});
let wall = new Wall(session.wallInfo);
brushBox.initialize(session.wallInfo.hakuLimits);
canvasRenderer.initialize(wall);
for (let onlineUser of session.wallInfo.online) {
@ -290,6 +292,45 @@ function readUrl(urlString) {
});
});
// Brush box
function updateBrushBoxDirtyState() {
if (brushBox.currentPresetId == null) return;
let currentPreset = brushBox.findBrushPreset(brushBox.currentPresetId);
if (brushEditor.name != currentPreset.name || brushEditor.code != currentPreset.code) {
brushBox.markDirty();
} else {
brushBox.markClean();
}
}
updateBrushBoxDirtyState();
brushEditor.addEventListener(".nameChanged", updateBrushBoxDirtyState);
brushEditor.addEventListener(".codeChanged", updateBrushBoxDirtyState);
brushBox.addEventListener(".clickNew", () => {
let id = brushBox.saveUserPreset({ name: brushEditor.name, code: brushEditor.code });
brushBox.updateBrushes();
brushBox.renderBrushes();
brushBox.setCurrentBrush(id);
brushBox.markClean();
});
brushBox.addEventListener(".brushChange", (event) => {
let preset = event.preset;
brushEditor.setName(preset.name);
brushEditor.setCode(preset.code);
brushBox.setCurrentBrush(preset.id);
brushBox.markClean();
});
brushBox.addEventListener(".overwritePreset", (event) => {
let preset = event.preset;
preset.name = brushEditor.name;
preset.code = brushEditor.code;
});
// Zoom indicator
canvasRenderer.addEventListener(".viewportUpdate", () => {

9
static/random.js Normal file
View file

@ -0,0 +1,9 @@
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
export function randomId(length = 32) {
let r = "";
for (let i = 0; i < length; ++i) {
r += charset.charAt(Math.floor(Math.random() * charset.length));
}
return r;
}

View file

@ -1,9 +1,7 @@
import { listen } from "rkgk/framework.js";
import { listen, SaveData } from "rkgk/framework.js";
export class ResizeHandle extends HTMLElement {
constructor() {
super();
}
saveData = new SaveData("resizeHandle");
connectedCallback() {
this.direction = this.getAttribute("data-direction");
@ -13,7 +11,13 @@ export class ResizeHandle extends HTMLElement {
this.initSize = parseInt(this.getAttribute("data-init-size"));
this.minSize = parseInt(this.getAttribute("data-min-size"));
this.#setSize(parseInt(localStorage.getItem(this.#localStorageKey)));
this.saveData.elementId = this.targetId;
// NOTE(localStorage): Migration from old local storage key.
let oldSizeKey = `rkgk.resizeHandle.size.${this.targetId}`;
this.#setSize(
this.saveData.get("size") ?? localStorage.getItem(oldSizeKey) ?? this.initSize,
);
this.#saveSize();
this.#updateTargetProperty();
@ -21,6 +25,9 @@ export class ResizeHandle extends HTMLElement {
this.visual.classList.add("visual");
this.#draggingBehaviour();
// NOTE(localStorage): Migration from old local storage key.
localStorage.removeItem(oldSizeKey);
}
#setSize(newSize) {
@ -30,12 +37,8 @@ export class ResizeHandle extends HTMLElement {
this.size = Math.max(this.minSize, newSize);
}
get #localStorageKey() {
return `rkgk.resizeHandle.size.${this.targetId}`;
}
#saveSize() {
localStorage.setItem(this.#localStorageKey, this.size);
this.saveData.set("size", this.size);
}
#updateTargetProperty() {
@ -53,9 +56,11 @@ export class ResizeHandle extends HTMLElement {
while (true) {
let event = await listen([window, "mousemove"], [window, "mouseup"]);
if (event.type == "mousemove") {
if (this.direction == "vertical") {
this.#setSize(startingSize + (mouseDown.clientX - event.clientX));
}
let delta =
this.direction == "vertical"
? mouseDown.clientX - event.clientX
: event.clientY - mouseDown.clientY;
this.#setSize(startingSize + delta);
this.#updateTargetProperty();
} else if (event.type == "mouseup") {
this.classList.remove("dragging");

View file

@ -22,6 +22,7 @@
<script type="module" async>
import "rkgk/live-reload.js";
import "rkgk/brush-box.js";
import "rkgk/brush-cost.js";
import "rkgk/brush-editor.js";
import "rkgk/brush-preview.js";
@ -77,10 +78,21 @@
data-direction="vertical"
data-target="panels-overlay"
data-target-property="--right-width"
data-init-size="512"
data-init-size="528"
data-min-size="384"></rkgk-resize-handle>
<div class="docked">
<rkgk-brush-editor></rkgk-brush-editor>
<rkgk-brush-box
id="brush-box"
data-storage-id="brush-box"></rkgk-brush-box>
<rkgk-resize-handle
data-direction="horizontal"
data-target="brush-box"
data-target-property="--height"
data-init-size="168"
data-min-size="96"></rkgk-resize-handle>
<rkgk-brush-editor
data-storage-id="brush-editor"></rkgk-brush-editor>
</div>
</div>
</div>
@ -140,6 +152,8 @@
</dialog>
</rkgk-connection-status>
<rkgk-context-menu-space class="fullscreen"></rkgk-context-menu-space>
<div class="fullscreen" id="js-loading">
<rkgk-throbber class="loading"></rkgk-throbber>