Add UI web components and adjust .gitignore

This commit is contained in:
2026-01-13 00:40:05 -05:00
parent 6f508ed194
commit b67235b93d
6 changed files with 594 additions and 1 deletions

View File

@@ -0,0 +1,211 @@
// CanvasDisplay component logic
// This file defines the custom element <canvas-display> which renders a canvas
// and an SVG grid overlay. It reacts to backgroundcolor and gridsettings
// events from the toolbar or directly via attributes.
class CanvasDisplay extends HTMLElement {
static get observedAttributes() {
return ["bg-color", "grid-color", "grid-size", "grid-numbering"];
}
constructor() {
super();
// Attach shadow root
this.attachShadow({ mode: "open" });
// Grab the template defined in canvas-display.html
const tmpl = document.getElementById("canvas-display-template");
if (!tmpl) {
console.error("CanvasDisplay: template not found");
return;
}
this.shadowRoot.appendChild(tmpl.content.cloneNode(true));
// Elements inside the shadow DOM
this._canvas = this.shadowRoot.querySelector("#draw-canvas");
this._gridOverlay = this.shadowRoot.querySelector("#grid-overlay");
// Canvas drawing context
this._ctx = this._canvas.getContext("2d");
// Bind handlers
this._onBgColorChange = this._onBgColorChange.bind(this);
this._onGridSettingsChange = this._onGridSettingsChange.bind(this);
this._onGridToggle = this._onGridToggle.bind(this);
}
connectedCallback() {
// Initialise size a default that can be overridden by CSS
this._canvas.width = this.clientWidth || 300;
this._canvas.height = this.clientHeight || 300;
// Apply default attribute values if none are set
if (!this.hasAttribute("bg-color"))
this.setAttribute("bg-color", "#ffffff");
if (!this.hasAttribute("grid-color"))
this.setAttribute("grid-color", "#000000");
if (!this.hasAttribute("grid-size")) this.setAttribute("grid-size", "25");
if (!this.hasAttribute("grid-numbering"))
this.setAttribute("grid-numbering", "true");
// Initial draw
this._updateCanvasBackground();
this._drawGrid();
// Listen for events bubbled up from slotted child components
this.addEventListener("bgcolorchange", this._onBgColorChange);
this.addEventListener("gridsettingschange", this._onGridSettingsChange);
this.addEventListener("gridtoggle", this._onGridToggle);
}
disconnectedCallback() {
this.removeEventListener("bgcolorchange", this._onBgColorChange);
this.removeEventListener("gridsettingschange", this._onGridSettingsChange);
this.removeEventListener("gridtoggle", this._onGridToggle);
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
switch (name) {
case "bg-color":
this._updateCanvasBackground();
break;
case "grid-color":
case "grid-size":
case "grid-numbering":
this._drawGrid();
break;
}
}
// ----- Property getters/setters -----
get bgColor() {
return this.getAttribute("bg-color");
}
set bgColor(v) {
this.setAttribute("bg-color", v);
}
get gridColor() {
return this.getAttribute("grid-color");
}
set gridColor(v) {
this.setAttribute("grid-color", v);
}
get gridSize() {
return Number(this.getAttribute("grid-size"));
}
set gridSize(v) {
this.setAttribute("grid-size", v);
}
get gridNumbering() {
return this.getAttribute("grid-numbering") === "true";
}
set gridNumbering(v) {
this.setAttribute("grid-numbering", v);
}
// ----- Event handlers -----
_onBgColorChange(e) {
const { color } = e.detail;
this.bgColor = color;
}
_onGridSettingsChange(e) {
const { color, size, numbering } = e.detail;
this.gridColor = color;
this.gridSize = size;
this.gridNumbering = numbering;
}
_onGridToggle(e) {
const { enabled } = e.detail;
this.gridNumbering = enabled;
// Reflect the change to the attribute so CSS/DOM stays in sync
this.setAttribute("grid-numbering", enabled);
}
// ----- Rendering helpers -----
_updateCanvasBackground() {
const { _ctx, _canvas, bgColor } = this;
_ctx.save();
_ctx.fillStyle = bgColor;
_ctx.fillRect(0, 0, _canvas.width, _canvas.height);
_ctx.restore();
}
_drawGrid() {
const { _gridOverlay, _canvas, gridColor, gridSize, gridNumbering } = this;
// Clear any previous grid
while (_gridOverlay.firstChild)
_gridOverlay.removeChild(_gridOverlay.firstChild);
const width = _canvas.width;
const height = _canvas.height;
const spacing = gridSize;
// Set SVG size and viewBox to match canvas dimensions
_gridOverlay.setAttribute("width", width);
_gridOverlay.setAttribute("height", height);
_gridOverlay.setAttribute("viewBox", `0 0 ${width} ${height}`);
// Helper to create a line
const makeLine = (x1, y1, x2, y2, thick) => {
const line = document.createElementNS(
"http://www.w3.org/2000/svg",
"line",
);
line.setAttribute("x1", x1);
line.setAttribute("y1", y1);
line.setAttribute("x2", x2);
line.setAttribute("y2", y2);
line.setAttribute("stroke", gridColor);
line.setAttribute("stroke-width", thick ? 2 : 1);
return line;
};
// Draw vertical lines
for (let x = 0; x <= width; x += spacing) {
const thick = (x / spacing) % 5 === 0;
_gridOverlay.appendChild(makeLine(x, 0, x, height, thick));
}
// Draw horizontal lines
for (let y = 0; y <= height; y += spacing) {
const thick = (y / spacing) % 5 === 0;
_gridOverlay.appendChild(makeLine(0, y, width, y, thick));
}
// Optional numbering
if (gridNumbering) {
const makeText = (x, y, txt) => {
const t = document.createElementNS(
"http://www.w3.org/2000/svg",
"text",
);
t.setAttribute("x", x + 2);
t.setAttribute("y", y + 10);
t.setAttribute("fill", gridColor);
t.setAttribute("font-size", "8");
t.textContent = txt;
return t;
};
// Number columns
for (let x = spacing; x <= width; x += spacing) {
const col = x / spacing;
if (col % 5 === 0) _gridOverlay.appendChild(makeText(x, 12, col));
}
// Number rows
for (let y = spacing; y <= height; y += spacing) {
const row = y / spacing;
if (row % 5 === 0) _gridOverlay.appendChild(makeText(2, y, row));
}
}
}
}
// Register the custom element
customElements.define("canvas-display", CanvasDisplay);