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);