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-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
									
								
							
							
						
						
									
										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";
 | 
			
		||||
 | 
			
		||||
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 = "";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										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
 | 
			
		||||
   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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										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 {
 | 
			
		||||
    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");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue