rkgk/static/brush-box.js

367 lines
10 KiB
JavaScript

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/dashes",
name: "Dashes",
code: `
color: #000
thickness: 4
length: 5
duty: 0.5
withDotter \\d ->
visible? = d.Num |mod length < length * duty
if (visible?)
stroke thickness color (line d.From d.To)
else
()
`.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 ->
pi = 3.14159265
a = sin (d.Num * wavelength / pi) + 1 / 2
range = maxThickness - minThickness
thickness = a * range + minThickness
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 ->
l = mag u
u / vec l l
perpClockwise: \\v ->
vec vecY.v -(vecX.v)
withDotter \\d ->
pi = 3.14159265
a = sin (d.Num * wavelength / pi) * amplitude
clockwise = norm (perpClockwise d.To-d.From) * vec a a
from = d.From + clockwise
to = d.To + clockwise
stroke thickness color (line from to)
`.trim(),
},
{
id: "builtin/rainbow",
name: "Rainbow",
code: `
wavelength: 0.1
thickness: 8
colorCurve: \\n ->
n |cos |abs
withDotter \\d ->
pi = 3.14159265
l = wavelength
r = colorCurve (d.Num * l)
g = colorCurve (d.Num * l + pi/3)
b = colorCurve (d.Num * l + 2*pi/3)
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.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>Switching to another brush will discard them." +
"<br>Are you sure?",
);
confirmation.addSeparator();
confirmation
.addButton("Discard changes and switch brush", {
classList: ["destructive"],
})
.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.info("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.info("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);