brush picker!
This commit is contained in:
parent
9b82b211b4
commit
c1612b2a94
12 changed files with 849 additions and 45 deletions
|
@ -9,6 +9,7 @@
|
||||||
--color-panel-border: rgba(0, 0, 0, 20%);
|
--color-panel-border: rgba(0, 0, 0, 20%);
|
||||||
--color-panel-background: #fff;
|
--color-panel-background: #fff;
|
||||||
--color-shaded-background: rgba(0, 0, 0, 5%);
|
--color-shaded-background: rgba(0, 0, 0, 5%);
|
||||||
|
--color-active-background: rgba(0, 0, 0, 10%);
|
||||||
|
|
||||||
--dialog-backdrop: rgba(255, 255, 255, 0.5);
|
--dialog-backdrop: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
@ -53,10 +54,28 @@ textarea {
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
border: 1px solid var(--color-panel-border);
|
border: 1px solid var(--color-panel-border);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
padding: 0.5rem 1.5rem;
|
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 {
|
button.icon {
|
||||||
|
|
348
static/brush-box.js
Normal file
348
static/brush-box.js
Normal 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);
|
|
@ -1,15 +1,10 @@
|
||||||
import { CodeEditor } from "rkgk/code-editor.js";
|
import { CodeEditor } from "rkgk/code-editor.js";
|
||||||
|
import { SaveData } from "rkgk/framework.js";
|
||||||
const defaultBrush = `
|
import { builtInPresets } from "rkgk/brush-box.js";
|
||||||
-- This is your brush.
|
|
||||||
-- Try playing around with the numbers,
|
|
||||||
-- and see what happens!
|
|
||||||
|
|
||||||
withDotter \\d ->
|
|
||||||
stroke 8 #000 (d To)
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
export class BrushEditor extends HTMLElement {
|
export class BrushEditor extends HTMLElement {
|
||||||
|
saveData = new SaveData("brushEditor");
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -17,6 +12,25 @@ export class BrushEditor extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.classList.add("rkgk-panel");
|
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(
|
this.codeEditor = this.appendChild(
|
||||||
new CodeEditor([
|
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) => {
|
this.codeEditor.addEventListener(".codeChanged", (event) => {
|
||||||
localStorage.setItem("rkgk.brushEditor.code", event.newCode);
|
this.saveData.set("code", event.newCode);
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
Object.assign(new Event(".codeChanged"), {
|
Object.assign(new Event(".codeChanged"), {
|
||||||
|
@ -41,12 +60,28 @@ export class BrushEditor extends HTMLElement {
|
||||||
|
|
||||||
this.errorArea = this.appendChild(document.createElement("pre"));
|
this.errorArea = this.appendChild(document.createElement("pre"));
|
||||||
this.errorArea.classList.add("errors");
|
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() {
|
get code() {
|
||||||
return this.codeEditor.code;
|
return this.codeEditor.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCode(newCode) {
|
||||||
|
this.codeEditor.setCode(newCode);
|
||||||
|
}
|
||||||
|
|
||||||
resetErrors() {
|
resetErrors() {
|
||||||
this.errorHeader.textContent = "";
|
this.errorHeader.textContent = "";
|
||||||
this.errorArea.textContent = "";
|
this.errorArea.textContent = "";
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
import { Pixmap } from "rkgk/haku.js";
|
import { Pixmap } from "rkgk/haku.js";
|
||||||
|
|
||||||
export class BrushPreview extends HTMLElement {
|
export class BrushPreview extends HTMLElement {
|
||||||
constructor() {
|
constructor(width, height) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.canvas = this.appendChild(document.createElement("canvas"));
|
this.canvas = this.appendChild(document.createElement("canvas"));
|
||||||
this.ctx = this.canvas.getContext("2d");
|
this.ctx = this.canvas.getContext("2d");
|
||||||
|
|
||||||
this.#resizeCanvas();
|
this.#resizeCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
#resizeCanvas() {
|
#resizeCanvas() {
|
||||||
this.canvas.width = this.clientWidth;
|
this.canvas.width = this.width ?? this.clientWidth;
|
||||||
this.canvas.height = this.clientHeight;
|
this.canvas.height = this.height ?? this.clientHeight;
|
||||||
|
|
||||||
if (this.pixmap != null) {
|
if (this.pixmap != null) {
|
||||||
this.pixmap.destroy();
|
this.pixmap.destroy();
|
||||||
|
@ -51,6 +55,7 @@ export class BrushPreview extends HTMLElement {
|
||||||
this.unsetErrorFlag();
|
this.unsetErrorFlag();
|
||||||
let result = await this.#renderBrushInner(haku);
|
let result = await this.#renderBrushInner(haku);
|
||||||
if (result.status == "error") {
|
if (result.status == "error") {
|
||||||
|
console.error(result);
|
||||||
this.setErrorFlag();
|
this.setErrorFlag();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -54,6 +54,10 @@ export class CodeEditor extends HTMLElement {
|
||||||
return this.textArea.value;
|
return this.textArea.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set code(newCode) {
|
||||||
|
this.textArea.value = newCode;
|
||||||
|
}
|
||||||
|
|
||||||
#codeChanged() {
|
#codeChanged() {
|
||||||
this.#resizeTextArea();
|
this.#resizeTextArea();
|
||||||
this.#renderLayers();
|
this.#renderLayers();
|
||||||
|
|
115
static/context-menu.js
Normal file
115
static/context-menu.js
Normal 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");
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
199
static/index.css
199
static/index.css
|
@ -1,6 +1,12 @@
|
||||||
/* index.css - styles for index.html and generally main parts of the app
|
/* index.css - styles for index.html and generally main parts of the app
|
||||||
For shared styles (such as color definitions) check out base.css. */
|
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 */
|
/* Main container layout */
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -90,9 +96,17 @@ main {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .docked {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > .docked > rkgk-brush-editor {
|
& > .docked > rkgk-brush-editor {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
pointer-events: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .floating {
|
& > .floating {
|
||||||
|
@ -144,25 +158,47 @@ main {
|
||||||
/* Resize handle */
|
/* Resize handle */
|
||||||
|
|
||||||
rkgk-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"] {
|
&[data-direction="vertical"] {
|
||||||
display: block;
|
width: var(--width);
|
||||||
width: 16px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
|
|
||||||
& > .visual {
|
& > .visual {
|
||||||
width: 2px;
|
width: var(--line-width);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-brand-blue);
|
|
||||||
margin: 0 auto;
|
|
||||||
opacity: 0%;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover > .visual,
|
& > .visual {
|
||||||
&.dragging > .visual {
|
background-color: var(--color-brand-blue);
|
||||||
opacity: 100%;
|
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 */
|
/* Code editor */
|
||||||
|
|
||||||
rkgk-code-editor {
|
rkgk-code-editor {
|
||||||
|
@ -258,8 +369,6 @@ rkgk-code-editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .layer-gutter {
|
& > .layer-gutter {
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
counter-reset: line;
|
counter-reset: line;
|
||||||
|
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
@ -318,10 +427,18 @@ rkgk-code-editor {
|
||||||
resize: none;
|
resize: none;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
border: none;
|
border: none;
|
||||||
|
background: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
/* The outline is displayed on the parent element to also surround the gutter. */
|
||||||
outline: none;
|
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) {
|
&:has(textarea:focus) {
|
||||||
|
@ -341,6 +458,11 @@ rkgk-brush-editor.rkgk-panel {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
& > .name {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
& > .text-area {
|
& > .text-area {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -389,8 +511,12 @@ rkgk-brush-preview {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
& > canvas {
|
& > canvas {
|
||||||
|
display: block;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
/* The brush preview doesn't scale with DPI as easily as the canvas renderer does,
|
/* The brush preview doesn't scale with DPI as easily as the canvas renderer does,
|
||||||
so instead we pixelate it. */
|
so instead we pixelate it. */
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
|
@ -487,8 +613,6 @@ rkgk-zoom-indicator.rkgk-panel {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
width: 6ch;
|
width: 6ch;
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-align: center;
|
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 */
|
||||||
|
|
||||||
.menu-bar {
|
.menu-bar {
|
||||||
|
|
|
@ -16,6 +16,7 @@ const updateInterval = 1000 / 60;
|
||||||
let main = document.querySelector("main");
|
let main = document.querySelector("main");
|
||||||
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
|
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 brushEditor = main.querySelector("rkgk-brush-editor");
|
let brushEditor = main.querySelector("rkgk-brush-editor");
|
||||||
let brushPreview = main.querySelector("rkgk-brush-preview");
|
let brushPreview = main.querySelector("rkgk-brush-preview");
|
||||||
let brushCostGauges = main.querySelector("rkgk-brush-cost-gauges");
|
let brushCostGauges = main.querySelector("rkgk-brush-cost-gauges");
|
||||||
|
@ -134,6 +135,7 @@ function readUrl(urlString) {
|
||||||
});
|
});
|
||||||
|
|
||||||
let wall = new Wall(session.wallInfo);
|
let wall = new Wall(session.wallInfo);
|
||||||
|
brushBox.initialize(session.wallInfo.hakuLimits);
|
||||||
canvasRenderer.initialize(wall);
|
canvasRenderer.initialize(wall);
|
||||||
|
|
||||||
for (let onlineUser of session.wallInfo.online) {
|
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
|
// Zoom indicator
|
||||||
|
|
||||||
canvasRenderer.addEventListener(".viewportUpdate", () => {
|
canvasRenderer.addEventListener(".viewportUpdate", () => {
|
||||||
|
|
9
static/random.js
Normal file
9
static/random.js
Normal 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;
|
||||||
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import { listen } from "rkgk/framework.js";
|
import { listen, SaveData } from "rkgk/framework.js";
|
||||||
|
|
||||||
export class ResizeHandle extends HTMLElement {
|
export class ResizeHandle extends HTMLElement {
|
||||||
constructor() {
|
saveData = new SaveData("resizeHandle");
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.direction = this.getAttribute("data-direction");
|
this.direction = this.getAttribute("data-direction");
|
||||||
|
@ -13,7 +11,13 @@ export class ResizeHandle extends HTMLElement {
|
||||||
this.initSize = parseInt(this.getAttribute("data-init-size"));
|
this.initSize = parseInt(this.getAttribute("data-init-size"));
|
||||||
this.minSize = parseInt(this.getAttribute("data-min-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.#saveSize();
|
||||||
this.#updateTargetProperty();
|
this.#updateTargetProperty();
|
||||||
|
|
||||||
|
@ -21,6 +25,9 @@ export class ResizeHandle extends HTMLElement {
|
||||||
this.visual.classList.add("visual");
|
this.visual.classList.add("visual");
|
||||||
|
|
||||||
this.#draggingBehaviour();
|
this.#draggingBehaviour();
|
||||||
|
|
||||||
|
// NOTE(localStorage): Migration from old local storage key.
|
||||||
|
localStorage.removeItem(oldSizeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setSize(newSize) {
|
#setSize(newSize) {
|
||||||
|
@ -30,12 +37,8 @@ export class ResizeHandle extends HTMLElement {
|
||||||
this.size = Math.max(this.minSize, newSize);
|
this.size = Math.max(this.minSize, newSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
get #localStorageKey() {
|
|
||||||
return `rkgk.resizeHandle.size.${this.targetId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
#saveSize() {
|
#saveSize() {
|
||||||
localStorage.setItem(this.#localStorageKey, this.size);
|
this.saveData.set("size", this.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateTargetProperty() {
|
#updateTargetProperty() {
|
||||||
|
@ -53,9 +56,11 @@ export class ResizeHandle extends HTMLElement {
|
||||||
while (true) {
|
while (true) {
|
||||||
let event = await listen([window, "mousemove"], [window, "mouseup"]);
|
let event = await listen([window, "mousemove"], [window, "mouseup"]);
|
||||||
if (event.type == "mousemove") {
|
if (event.type == "mousemove") {
|
||||||
if (this.direction == "vertical") {
|
let delta =
|
||||||
this.#setSize(startingSize + (mouseDown.clientX - event.clientX));
|
this.direction == "vertical"
|
||||||
}
|
? mouseDown.clientX - event.clientX
|
||||||
|
: event.clientY - mouseDown.clientY;
|
||||||
|
this.#setSize(startingSize + delta);
|
||||||
this.#updateTargetProperty();
|
this.#updateTargetProperty();
|
||||||
} else if (event.type == "mouseup") {
|
} else if (event.type == "mouseup") {
|
||||||
this.classList.remove("dragging");
|
this.classList.remove("dragging");
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
<script type="module" async>
|
<script type="module" async>
|
||||||
import "rkgk/live-reload.js";
|
import "rkgk/live-reload.js";
|
||||||
|
|
||||||
|
import "rkgk/brush-box.js";
|
||||||
import "rkgk/brush-cost.js";
|
import "rkgk/brush-cost.js";
|
||||||
import "rkgk/brush-editor.js";
|
import "rkgk/brush-editor.js";
|
||||||
import "rkgk/brush-preview.js";
|
import "rkgk/brush-preview.js";
|
||||||
|
@ -77,10 +78,21 @@
|
||||||
data-direction="vertical"
|
data-direction="vertical"
|
||||||
data-target="panels-overlay"
|
data-target="panels-overlay"
|
||||||
data-target-property="--right-width"
|
data-target-property="--right-width"
|
||||||
data-init-size="512"
|
data-init-size="528"
|
||||||
data-min-size="384"></rkgk-resize-handle>
|
data-min-size="384"></rkgk-resize-handle>
|
||||||
<div class="docked">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,6 +152,8 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
</rkgk-connection-status>
|
</rkgk-connection-status>
|
||||||
|
|
||||||
|
<rkgk-context-menu-space class="fullscreen"></rkgk-context-menu-space>
|
||||||
|
|
||||||
<div class="fullscreen" id="js-loading">
|
<div class="fullscreen" id="js-loading">
|
||||||
<rkgk-throbber class="loading"></rkgk-throbber>
|
<rkgk-throbber class="loading"></rkgk-throbber>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue