diff --git a/.changeset/thick-lions-smile.md b/.changeset/thick-lions-smile.md new file mode 100644 index 00000000..a309e543 --- /dev/null +++ b/.changeset/thick-lions-smile.md @@ -0,0 +1,5 @@ +--- +"@jolly-pixel/pixel-draw.renderer": minor +--- + +Add a `backgroundColor` option/property to `PixelArtCanvas`, letting callers set the canvas void color explicitly instead of relying solely on the parent element's inferred CSS `background-color` diff --git a/packages/pixel-draw-renderer/docs/PixelArtCanvas.md b/packages/pixel-draw-renderer/docs/PixelArtCanvas.md index b956bcec..9f07b907 100644 --- a/packages/pixel-draw-renderer/docs/PixelArtCanvas.md +++ b/packages/pixel-draw-renderer/docs/PixelArtCanvas.md @@ -44,6 +44,12 @@ interface PixelArtCanvasOptions { colors: { odd: string; even: string; }; squareSize: number; }; + /** + * Fill color for the canvas area outside the texture bounds (the "void" + * around the drawing surface). Defaults to the parent element's own CSS + * `background-color` if it's set and non-transparent, else `#424242`. + */ + backgroundColor?: ColorInput; brush?: BrushOptions; select?: { /** @@ -89,7 +95,7 @@ interface PixelArtCanvasOptions { Undocumented defaults: `texture.size` is `{ x: 64, y: 32 }` (`y` falls back to `x` when only `x` is given), `texture.maxSize` is `2048`, `zoom.default` is `4`, `zoom.min`/`zoom.max` are `1`/`32`, `zoom.sensitivity` is `0.1`, `backgroundTransparency.squareSize` is `8`, `backgroundTransparency.colors` is `{ odd: "#999", even: "#666" }`. -The background color used behind transparent texture pixels is read from `getComputedStyle(parentHtmlElement).backgroundColor` at construction time (falling back to `#555555` if unset or fully transparent); it isn't a configurable option. +The `backgroundColor` option, if given, wins outright. Otherwise it's read from `getComputedStyle(parentHtmlElement).backgroundColor` at construction time, falling back to `#424242` if that's unset or fully transparent. See the `backgroundColor` property below to change it after construction. ## Properties @@ -141,6 +147,17 @@ A global fill is still committed and undoable as a single atomic edit, but is br --- +### `backgroundColor` + +```ts +get backgroundColor(): string +set backgroundColor(color: ColorInput) +``` + +Reads or changes the fill color for the canvas area outside the texture bounds — see the `backgroundColor` constructor option above for how the initial value is resolved. The setter takes effect immediately (redraws the canvas itself); no `drawFrame()` call needed. + +--- + ### `textureSize` ```ts diff --git a/packages/pixel-draw-renderer/examples/public/main.css b/packages/pixel-draw-renderer/examples/public/main.css index 3b57d8dc..66bc0a73 100644 --- a/packages/pixel-draw-renderer/examples/public/main.css +++ b/packages/pixel-draw-renderer/examples/public/main.css @@ -33,18 +33,38 @@ body { } .resize-handle.left { - width: 4px; + position: relative; + width: 8px; flex-shrink: 0; background: #1a2228; cursor: col-resize; transition: background 0.15s; } +.resize-handle.left::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 3px; + height: 22px; + transform: translate(-50%, -50%); + background-image: radial-gradient(circle, #6d8794 1.1px, transparent 1.3px); + background-size: 100% 7px; + background-repeat: repeat-y; + pointer-events: none; +} + .resize-handle.left:hover, html.handle-dragging.vertical .resize-handle.left { background: #4488ff; } +.resize-handle.left:hover::after, +html.handle-dragging.vertical .resize-handle.left::after { + background-image: radial-gradient(circle, #ffffff 1.1px, transparent 1.3px); +} + html.handle-dragging.vertical { cursor: col-resize; } diff --git a/packages/pixel-draw-renderer/examples/scripts/main.ts b/packages/pixel-draw-renderer/examples/scripts/main.ts index 421e95e6..9459d8f9 100644 --- a/packages/pixel-draw-renderer/examples/scripts/main.ts +++ b/packages/pixel-draw-renderer/examples/scripts/main.ts @@ -37,16 +37,22 @@ async function initRuntime(): Promise { const drawPanel = document.querySelector("pixel-draw-panel")!; const canvasManager = await drawPanel.initialize({ texture: { - size: { x: 16, y: 16 } + size: { + x: 16, + y: 16 + } }, defaultMode: "paint", + backgroundColor: "#263238", zoom: { default: 16, min: 1, max: 32, sensitivity: 1 }, - brush: { size: 1 }, + brush: { + size: 1 + }, history: { enabled: true } diff --git a/packages/pixel-draw-renderer/examples/scripts/ui/ColorSwatch.ts b/packages/pixel-draw-renderer/examples/scripts/ui/ColorSwatch.ts index 68de5582..d9b2d049 100644 --- a/packages/pixel-draw-renderer/examples/scripts/ui/ColorSwatch.ts +++ b/packages/pixel-draw-renderer/examples/scripts/ui/ColorSwatch.ts @@ -24,11 +24,12 @@ export class ColorSwatch extends LitElement { button { width: 26px; height: 26px; - border: 2px solid var(--color-swatch-border, #666); + border: 2px solid var(--color-swatch-border, #556067); border-radius: var(--color-swatch-radius, 4px); cursor: pointer; padding: 0; background: #000000; + box-sizing: border-box; } button:focus-visible { @@ -152,16 +153,24 @@ export class ColorSwatch extends LitElement { } } + /** + * Closes the picker if open. Lets a container holding multiple swatches + * (e.g. foreground/background) enforce only one open at a time. + */ + close(): void { + this.#setOpen(false); + } + #setOpen( open: boolean ): void { this.#open = open; const portal = this.#portal!; if (open) { - const rect = this.#buttonEl!.getBoundingClientRect(); - portal.style.left = `${rect.left}px`; - portal.style.top = `${rect.bottom + 4}px`; portal.style.display = ""; + this.#positionPortal(); + // Lets a container close sibling swatches so only one picker is open at a time. + this.dispatchEvent(new CustomEvent("swatch-opened", { bubbles: true, composed: true })); } else { portal.style.display = "none"; @@ -169,6 +178,30 @@ export class ColorSwatch extends LitElement { this.requestUpdate(); } + /** + * Flips the popup above the button when there isn't enough room below (and + * clamps both axes to the viewport) instead of letting it run off-screen. + * getBoundingClientRect() forces the layout needed to measure the portal's + * real size right after `display` is unhidden. + */ + #positionPortal(): void { + const portal = this.#portal!; + const margin = 4; + const buttonRect = this.#buttonEl!.getBoundingClientRect(); + const portalRect = portal.getBoundingClientRect(); + + const fitsBelow = window.innerHeight - buttonRect.bottom >= portalRect.height + margin; + const top = fitsBelow + ? buttonRect.bottom + margin + : Math.max(margin, buttonRect.top - portalRect.height - margin); + + const maxLeft = window.innerWidth - portalRect.width - margin; + const left = Math.min(buttonRect.left, Math.max(margin, maxLeft)); + + portal.style.top = `${top}px`; + portal.style.left = `${left}px`; + } + readonly #onSwatchClick = ( event: MouseEvent ): void => { diff --git a/packages/pixel-draw-renderer/examples/scripts/ui/PixelDrawPanel.ts b/packages/pixel-draw-renderer/examples/scripts/ui/PixelDrawPanel.ts index a51d2db7..4f2967a1 100644 --- a/packages/pixel-draw-renderer/examples/scripts/ui/PixelDrawPanel.ts +++ b/packages/pixel-draw-renderer/examples/scripts/ui/PixelDrawPanel.ts @@ -1,5 +1,5 @@ // Import Third-party Dependencies -import { LitElement, html, css } from "lit"; +import { LitElement, html, css, nothing } from "lit"; import { customElement } from "lit/decorators.js"; // Import Internal Dependencies @@ -9,79 +9,233 @@ import { type Mode } from "../../../src/index.ts"; import { type ColorSwatch, type ColorChangeDetail } from "./ColorSwatch.ts"; +import { renderIcon, type IconName } from "./icons.ts"; + +// CONSTANTS +const kModeItems: { mode: Mode; icon: IconName; label: string; }[] = [ + { mode: "move", icon: "move", label: "Move" }, + { mode: "paint", icon: "paint", label: "Paint" }, + { mode: "fill", icon: "fill", label: "Fill" }, + { mode: "select", icon: "select", label: "Select" } +]; @customElement("pixel-draw-panel") export class PixelDrawPanel extends LitElement { static override styles = css` :host { display: flex; - flex-direction: column; + flex-direction: row; height: 100%; } - .toolbar { + .rail { position: relative; - z-index: 2; + z-index: 3; display: flex; + flex-direction: column; align-items: center; - flex-wrap: wrap; - gap: 4px 12px; - padding: 6px 12px; + justify-content: center; + width: 60px; + flex-shrink: 0; + padding: 10px 0; + gap: 10px; background: #37474F; color: #eee; - font-size: 12px; font-family: sans-serif; user-select: none; - flex-shrink: 0; } - .toolbar-item { + .rail-section { display: flex; + flex-direction: column; align-items: center; - gap: 6px; - cursor: default; + flex-shrink: 0; + gap: 4px; } - .mode-btn { - padding: 3px 10px; - border: 1px solid #555; - border-radius: 3px; + .rail-divider { + width: 32px; + height: 1px; + flex-shrink: 0; + background: #4b5b63; + } + + .rail-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 36px; + height: 36px; + padding: 0; + border: 1px solid transparent; + border-radius: 4px; background: transparent; - color: #aaa; - font-size: 11px; - font-family: sans-serif; + color: #ccc; cursor: pointer; } - .mode-btn:hover { - background: #3a3a3a; - color: #eee; + .rail-btn:hover:not(:disabled) { + color: #fff; } - .mode-btn.active { + .rail-btn.active { background: #4488ff; border-color: #4488ff; color: #fff; } + .rail-btn:disabled { + color: #556067; + cursor: default; + } - .toolbar-item input[type="range"] { - width: 80px; - min-width: 48px; - flex-shrink: 1; - cursor: pointer; + .icon { + width: 21px; + height: 21px; + flex-shrink: 0; + } + .swap-btn .icon { + width: 11px; + height: 11px; } - .toolbar-item span { - width: 32px; - text-align: right; + .tooltip { + position: absolute; + left: calc(100% + 8px); + top: 50%; + z-index: 10; + padding: 3px 8px; + border-radius: 3px; + background: #1d262b; + color: #eee; font-size: 11px; - color: #ccc; + white-space: nowrap; + pointer-events: none; + opacity: 0; + visibility: hidden; + transform: translateY(-50%); + transition: opacity 0.1s ease; + } + .rail-btn:hover .tooltip { + opacity: 1; + visibility: visible; } - .canvas-host { - flex: 1; + .color-picker { + /* + * Self-contained box: fg/bg swatches and the swap button all stay + * within these bounds (no negative offsets), so the rail's own gap + * is the true, symmetric visual spacing above/below this element. + * flex-shrink:0 matters here specifically: its children are all + * position:absolute, so it has ~0 natural content height and would + * otherwise be the first thing the flex column crushes when the rail + * runs short on vertical space, leaving the swatches anchored to a + * collapsed box and overlapped by neighboring elements. + */ position: relative; - overflow: hidden; + flex-shrink: 0; + width: 44px; + height: 44px; + } + + .color-picker .swatch { + position: absolute; + } + .color-picker .swatch.fg { + top: 4px; + left: 0; + z-index: 2; + } + .color-picker .swatch.bg { + right: 0; + bottom: 0; + z-index: 1; + } + .color-picker .swatch::part(swatch) { + width: 24px; + height: 24px; + } + + .swap-btn { + position: absolute; + top: 0; + right: 0; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: 50%; + background: #546b76; + color: #fff; + font-size: 9px; + line-height: 1; + cursor: pointer; + } + .swap-btn:hover { + background: #4488ff; + } + + .stage { + position: relative; + flex: 1; min-width: 0; min-height: 0; + overflow: hidden; + } + + .canvas-host { + /* + * PixelArtCanvas.appendTo() sets this element's position to "relative" + * inline (higher specificity than this stylesheet), so sizing must + * come from width/height, not position:absolute + inset. + */ + width: 100%; + height: 100%; + } + + .tool-option-overlay { + position: absolute; + top: 8px; + left: 50%; + z-index: 2; + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 12px; + background: rgba(30, 38, 43, 0.85); + color: #eee; + font-size: 11px; + font-family: sans-serif; + user-select: none; + transform: translateX(-50%); + } + + .tool-option-overlay input[type="range"] { + width: 100px; + cursor: pointer; + } + + .tool-option-overlay span { + width: 28px; + text-align: right; + } + + .fill-toggle-btn { + padding: 2px 8px; + border: 1px solid #556067; + border-radius: 10px; + background: transparent; + color: #eee; + font-size: 11px; + cursor: pointer; + } + .fill-toggle-btn.active { + background: #4488ff; + border-color: #4488ff; } `; @@ -90,7 +244,11 @@ export class PixelDrawPanel extends LitElement { // requestUpdate() instead of the decorator. #mode: Mode = "paint"; #brushSize = 1; - #zoomSensitivity = 0.6; + #fillGlobal = false; + #foreground: ColorChangeDetail = { hex: "#000000", opacity: 1 }; + #background: ColorChangeDetail = { hex: "#ffffff", opacity: 1 }; + #canUndo = false; + #canRedo = false; #canvasManager: PixelArtCanvas | null = null; get canvasManager(): PixelArtCanvas | null { @@ -100,6 +258,7 @@ export class PixelDrawPanel extends LitElement { override connectedCallback() { super.connectedCallback(); this.addEventListener("colorpicked", this.#onColorPicked); + this.addEventListener("swatch-opened", this.#onSwatchOpened); } /** @@ -113,13 +272,28 @@ export class PixelDrawPanel extends LitElement { await this.updateComplete; const canvasHostEl = this.shadowRoot!.querySelector(".canvas-host")!; - this.#canvasManager = new PixelArtCanvas(canvasHostEl, options); + this.#canvasManager = new PixelArtCanvas(canvasHostEl, { + ...options, + onHistoryChange: (state) => { + this.#canUndo = state.canUndo; + this.#canRedo = state.canRedo; + this.requestUpdate(); + options.onHistoryChange?.(state); + } + }); - // Sync toolbar state with whatever defaults were passed in options. + // Sync rail state with whatever defaults were passed in options. this.#mode = this.#canvasManager.mode; this.#brushSize = this.#canvasManager.brush.size; - this.#zoomSensitivity = this.#canvasManager.zoomSensitivity; + this.#fillGlobal = this.#canvasManager.fillGlobal; + this.#foreground = { + hex: this.#canvasManager.brush.colorAsString("hex"), + opacity: this.#canvasManager.brush.opacity + }; + this.#canUndo = this.#canvasManager.canUndo(); + this.#canRedo = this.#canvasManager.canRedo(); this.requestUpdate(); + this.#syncForegroundSwatch(); return this.#canvasManager; } @@ -131,6 +305,7 @@ export class PixelDrawPanel extends LitElement { override disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener("colorpicked", this.#onColorPicked); + this.removeEventListener("swatch-opened", this.#onSwatchOpened); this.#canvasManager?.destroy(); this.#canvasManager = null; } @@ -156,73 +331,98 @@ export class PixelDrawPanel extends LitElement { this.requestUpdate(); } - #onZoomSensitivityChange( - event: Event - ): void { - const value = parseFloat((event.target as HTMLInputElement).value); - this.#zoomSensitivity = value; + #onFillGlobalToggle(): void { + this.#fillGlobal = !this.#fillGlobal; if (this.#canvasManager) { - this.#canvasManager.zoomSensitivity = value; + this.#canvasManager.fillGlobal = this.#fillGlobal; } this.requestUpdate(); } - #onColorSwatchChange( + #onForegroundChange( event: CustomEvent ): void { - const { hex, opacity } = event.detail; - this.#canvasManager?.brush.color(hex, opacity); + this.#foreground = event.detail; + this.#canvasManager?.brush.color(event.detail.hex, event.detail.opacity); + } + + #onBackgroundChange( + event: CustomEvent + ): void { + this.#background = event.detail; + } + + /** + * Exchanges foreground/background, pushing the new foreground onto the + * brush. Background stays inert until it becomes the foreground. + */ + #swapColors(): void { + [this.#foreground, this.#background] = [this.#background, this.#foreground]; + this.#canvasManager?.brush.color(this.#foreground.hex, this.#foreground.opacity); + this.requestUpdate(); + this.#syncForegroundSwatch(); + this.#syncBackgroundSwatch(); + } + + /** + * ColorSwatch only paints its swatch/picker from `color`/`opacity` in + * firstUpdated() — later property changes made from here need an explicit + * setColor() to actually repaint it (mirrors #onColorPicked below). + */ + #syncForegroundSwatch(): void { + this.shadowRoot?.querySelector("color-swatch.fg") + ?.setColor(this.#foreground.hex, this.#foreground.opacity); + } + + #syncBackgroundSwatch(): void { + this.shadowRoot?.querySelector("color-swatch.bg") + ?.setColor(this.#background.hex, this.#background.opacity); + } + + #onUndo(): void { + this.#canvasManager?.undo(); + } + + #onRedo(): void { + this.#canvasManager?.redo(); } /** - * Mirrors a color picked via the canvas eyedropper (right-click) back onto - * the color-swatch, without re-triggering its "color-change" event. + * Mirrors a color picked via the canvas eyedropper (right-click) onto the + * foreground swatch, without re-triggering its "color-change" event. */ readonly #onColorPicked = ( event: Event ): void => { const { hex, opacity } = ( - event as CustomEvent<{ hex: string; opacity: number; }> + event as CustomEvent ).detail; - this.shadowRoot!.querySelector("color-swatch")!.setColor(hex, opacity); + this.#foreground = { hex, opacity }; + this.shadowRoot!.querySelector("color-swatch.fg")!.setColor(hex, opacity); }; - override render() { - return html` -
-
- - - - -
- -
- -
+ /** + * Closes the sibling swatch so foreground/background pickers can't both be + * open. Crossing the color-swatch's shadow boundary retargets event.target + * to this panel itself, so composedPath()[0] is used to find the actual + * swatch that opened. + */ + readonly #onSwatchOpened = ( + event: Event + ): void => { + const opened = event.composedPath()[0]; + for (const swatch of this.shadowRoot!.querySelectorAll("color-swatch")) { + if (swatch !== opened) { + swatch.close(); + } + } + }; -
+ `; + } + + return nothing; + } + + override render() { + return html` +
+
+ ${kModeItems.map(({ mode, icon, label }) => html` + + `)} +
+ +
+ +
+ + + +
+ +
+ +
+ + +
-
+
+
+ ${this.#renderToolOptions()} +
`; } } diff --git a/packages/pixel-draw-renderer/examples/scripts/ui/icons.ts b/packages/pixel-draw-renderer/examples/scripts/ui/icons.ts new file mode 100644 index 00000000..6569a1b6 --- /dev/null +++ b/packages/pixel-draw-renderer/examples/scripts/ui/icons.ts @@ -0,0 +1,73 @@ +/* eslint-disable @stylistic/max-len */ +// Import Third-party Dependencies +import { svg, type SVGTemplateResult } from "lit"; + +export type IconName = "move" | "paint" | "fill" | "select" | "undo" | "redo" | "swap"; + +// CONSTANTS +const kIcons: Record = { + // Pan/grab hand: four splayed fingers over a rounded palm. + move: svg` + + + + + + `, + // Feather "edit-2" pencil. + paint: svg` + + `, + // Tilted paint bucket with a rim line and a paint drip. + fill: svg` + + + + + + + `, + // Marquee selection: dashed rectangle with corner handles. + select: svg` + + + + + + `, + undo: svg` + + + `, + redo: svg` + + + `, + swap: svg` + + + + + ` +}; + +/** + * Renders a named icon as a self-contained . Colors follow the + * button's CSS `color` via `currentColor`, so hover/active states need no + * icon-specific styling. + */ +export function renderIcon( + name: IconName +): SVGTemplateResult { + return svg` + + `; +} diff --git a/packages/pixel-draw-renderer/src/PixelArtCanvas.ts b/packages/pixel-draw-renderer/src/PixelArtCanvas.ts index f12630e4..2abef3b0 100644 --- a/packages/pixel-draw-renderer/src/PixelArtCanvas.ts +++ b/packages/pixel-draw-renderer/src/PixelArtCanvas.ts @@ -81,6 +81,13 @@ export interface PixelArtCanvasOptions { colors: { odd: string; even: string; }; squareSize: number; }; + /** + * Fill color for the canvas area outside the texture bounds (the "void" + * around the drawing surface). Defaults to the parent element's own CSS + * `background-color` if it's set and non-transparent, else `#424242`. + * Can also be changed after construction via the `backgroundColor` setter. + */ + backgroundColor?: ColorInput; brush?: BrushOptions; select?: { /** @@ -169,9 +176,11 @@ export class PixelArtCanvas { }); const computedBackgroundColor = getComputedStyle(parentHtmlElement).backgroundColor; - const backgroundColor = computedBackgroundColor && new Color(computedBackgroundColor).alpha > 0 - ? computedBackgroundColor - : "#555555"; + const backgroundColor = options.backgroundColor ?? ( + computedBackgroundColor && new Color(computedBackgroundColor).alpha > 0 + ? computedBackgroundColor + : "#424242" + ); this.#renderer = new CanvasRenderer({ viewport: this.#viewport, @@ -303,6 +312,21 @@ export class PixelArtCanvas { this.#tools.fill.global = global; } + /** + * Fill color for the canvas area outside the texture bounds. See the + * `backgroundColor` constructor option for the initial-value fallback + * chain; this setter always applies immediately, no `drawFrame()` needed. + */ + get backgroundColor(): string { + return this.#renderer.backgroundColor; + } + + set backgroundColor( + color: ColorInput + ) { + this.#renderer.backgroundColor = color; + } + get parentHtmlElement(): HTMLDivElement { return this.#parentHtmlElement; } diff --git a/packages/pixel-draw-renderer/src/rendering/CanvasRenderer.ts b/packages/pixel-draw-renderer/src/rendering/CanvasRenderer.ts index decbf1e9..1f234976 100644 --- a/packages/pixel-draw-renderer/src/rendering/CanvasRenderer.ts +++ b/packages/pixel-draw-renderer/src/rendering/CanvasRenderer.ts @@ -82,6 +82,17 @@ export class CanvasRenderer { return this.#canvas; } + get backgroundColor(): string { + return this.#backgroundColor; + } + + set backgroundColor( + color: ColorInput + ) { + this.#backgroundColor = new Color(color).toString(); + this.drawFrame(); + } + drawFrame(): void { if (this.#canvas.width === 0 || this.#canvas.height === 0) { return; diff --git a/packages/pixel-draw-renderer/test/PixelArtCanvas.spec.ts b/packages/pixel-draw-renderer/test/PixelArtCanvas.spec.ts index b761a7f2..be83caad 100644 --- a/packages/pixel-draw-renderer/test/PixelArtCanvas.spec.ts +++ b/packages/pixel-draw-renderer/test/PixelArtCanvas.spec.ts @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; // Import Third-party Dependencies import { Window } from "happy-dom"; +import Color from "colorjs.io"; // Import Internal Dependencies import { PixelArtCanvas } from "../src/PixelArtCanvas.ts"; @@ -105,6 +106,37 @@ describe("PixelArtCanvas", () => { }); }); + describe("backgroundColor", () => { + test("defaults to the parent element's computed CSS background-color", () => { + const manager = new PixelArtCanvas(container, { + texture: { maxSize: 32, size: { x: 8, y: 8 } } + }); + + assert.strictEqual(manager.backgroundColor, new Color("#555555").toString()); + manager.destroy(); + }); + + test("backgroundColor option overrides the CSS-inferred default", () => { + const manager = new PixelArtCanvas(container, { + texture: { maxSize: 32, size: { x: 8, y: 8 } }, + backgroundColor: "#ff0000" + }); + + assert.strictEqual(manager.backgroundColor, new Color("#ff0000").toString()); + manager.destroy(); + }); + + test("setting backgroundColor updates the returned value", () => { + const manager = new PixelArtCanvas(container, { + texture: { maxSize: 32, size: { x: 8, y: 8 } } + }); + + manager.backgroundColor = "#00ff00"; + assert.strictEqual(manager.backgroundColor, new Color("#00ff00").toString()); + manager.destroy(); + }); + }); + describe("destroy", () => { test("destroy() does not throw", () => { const manager = new PixelArtCanvas(container, {