brush picker!
This commit is contained in:
parent
9b82b211b4
commit
c1612b2a94
12 changed files with 849 additions and 45 deletions
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);
|
Loading…
Add table
Add a link
Reference in a new issue