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.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(
"Your brush has unsaved changes." +
"
Switching to another brush will discard them." +
"
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.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);