Files
picoGraph/scripts/ui/ColorSwatchPicker.js
Max Litruv Boonzaayer f917cf6ad5 Add initial Pico-8 cartridges for testing
- Created test_cart.p8 with basic drawing functionality and a print statement.
- Created test_unix.p8 with a simple initialization function.
2026-03-21 18:38:43 +11:00

185 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Ordered PICO-8 palette hex strings, indices 015.
*
* @type {ReadonlyArray<string>}
*/
const PICO8_COLORS = Object.freeze([
'#000002', '#1D2B53', '#7E2553', '#008751',
'#AB5236', '#5F574F', '#C2C3C7', '#FFF1E8',
'#FF004D', '#FFA300', '#FFEC27', '#00E436',
'#29ADFF', '#83769C', '#FF77A8', '#FFCCAA',
]);
/**
* @typedef {{ element: HTMLElement, setValue: (index: number) => void }} ColorSwatchPickerInstance
*/
/**
* Builds an inline PICO-8 color swatch picker control.
*
* Renders a colored trigger button that opens a 4×4 grid of all 16 PICO-8
* palette swatches. Clicking a swatch fires `onChange` with the selected
* palette index (015) and closes the dropdown.
*
* @param {{
* id?: string,
* value: number,
* onChange: (index: number) => void,
* }} options Picker configuration.
* @returns {ColorSwatchPickerInstance}
*/
export function createColorSwatchPicker({ id, value, onChange }) {
const wrapper = document.createElement('div');
wrapper.className = 'color-swatch-picker';
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'color-swatch-trigger';
if (id) {
trigger.id = id;
}
trigger.setAttribute('aria-haspopup', 'menu');
trigger.setAttribute('aria-expanded', 'false');
wrapper.appendChild(trigger);
const dropdown = document.createElement('div');
dropdown.className = 'color-swatch-dropdown';
dropdown.setAttribute('role', 'menu');
// Appended to body on open so it escapes overflow:hidden on node cards.
/** @type {Array<HTMLButtonElement>} */
const swatchButtons = [];
/** @type {((event: MouseEvent) => void) | null} */
let outsideClickHandler = null;
/** @type {(() => void) | null} */
let scrollAbortHandler = null;
const repositionDropdown = () => {
const rect = trigger.getBoundingClientRect();
dropdown.style.top = `${rect.bottom + 4}px`;
dropdown.style.left = `${rect.left}px`;
};
const closeDropdown = () => {
if (dropdown.isConnected) {
dropdown.remove();
}
trigger.setAttribute('aria-expanded', 'false');
if (outsideClickHandler) {
document.removeEventListener('click', outsideClickHandler, true);
outsideClickHandler = null;
}
if (scrollAbortHandler) {
document.removeEventListener('scroll', scrollAbortHandler, true);
scrollAbortHandler = null;
}
};
const openDropdown = () => {
repositionDropdown();
document.body.appendChild(dropdown);
trigger.setAttribute('aria-expanded', 'true');
outsideClickHandler = (event) => {
if (
!(event.target instanceof Node) ||
(!wrapper.contains(event.target) && !dropdown.contains(event.target))
) {
closeDropdown();
}
};
scrollAbortHandler = () => closeDropdown();
document.addEventListener('click', outsideClickHandler, true);
document.addEventListener('scroll', scrollAbortHandler, true);
};
let currentIndex = clampColorIndex(value);
/**
* Updates the trigger button to reflect the supplied palette index.
*
* @param {number} index Palette index (015).
*/
const updateTrigger = (index) => {
const hex = PICO8_COLORS[index] ?? '#000000';
trigger.style.backgroundColor = hex;
trigger.title = `Color ${index}`;
trigger.setAttribute('aria-label', `Color ${index}`);
};
PICO8_COLORS.forEach((hex, index) => {
const swatch = document.createElement('button');
swatch.type = 'button';
swatch.className = 'color-swatch-option';
swatch.style.backgroundColor = hex;
swatch.dataset.colorIndex = String(index);
swatch.title = String(index);
swatch.setAttribute('role', 'menuitemradio');
swatch.setAttribute('aria-label', `Color ${index}`);
swatch.setAttribute('aria-checked', 'false');
swatch.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
swatch.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const selected = index;
closeDropdown();
if (selected !== currentIndex) {
currentIndex = selected;
updateTrigger(selected);
swatchButtons.forEach((btn, i) => {
btn.setAttribute('aria-checked', i === selected ? 'true' : 'false');
});
onChange(selected);
}
});
dropdown.appendChild(swatch);
swatchButtons.push(swatch);
});
trigger.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
trigger.addEventListener('click', (event) => {
event.stopPropagation();
if (dropdown.isConnected) {
closeDropdown();
} else {
openDropdown();
}
});
/**
* Updates the picker display without firing `onChange`.
*
* @param {number} newIndex Palette index (015).
*/
const setValue = (newIndex) => {
currentIndex = clampColorIndex(newIndex);
updateTrigger(currentIndex);
swatchButtons.forEach((btn, i) => {
btn.setAttribute('aria-checked', i === currentIndex ? 'true' : 'false');
});
};
setValue(currentIndex);
return { element: wrapper, setValue };
}
/**
* Clamps an arbitrary value to a valid PICO-8 palette index (015).
*
* @param {unknown} value Raw index candidate.
* @returns {number}
*/
function clampColorIndex(value) {
const n = typeof value === 'number' ? value : Number(value);
const safe = Number.isFinite(n) ? Math.round(n) : 0;
return Math.max(0, Math.min(15, safe));
}