367 lines
10 KiB
JavaScript
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);
|