Back to Powder Tool V600Billion
SOURCE / PINNED RELEASE

Made of little things.

Powder Tool V600Billion

Release
142767edcab8…
Author-recorded commit
6d92971effd0…
License
LICENSE
Author’s source reference
nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy

Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

source/src/ui/palette.js
/**
 * The element palette: the game's tools by section, a search over all of
 * them, and which tool each mouse button draws with. Clicking a tool gives it
 * to the left button, right-clicking to the right one, as in the game.
 */
import { ERASER, SECTIONS, inkFor } from './catalog.js';

const PAINT = 15;

export function createPalette({ root, tools, pick, onPaintColour }) {
  const byId = new Map(tools.map((tool) => [tool.id, tool]));
  const sectionsNav = root.querySelector('#sections');
  const grid = root.querySelector('#tools');
  const search = root.querySelector('#tool-search');
  const paint = root.querySelector('#paint');
  const slots = [root.querySelector('#slot-0'), root.querySelector('#slot-1')];
  let section = 8;
  let picked = ['', ''];

  const present = SECTIONS.filter((s) => tools.some((tool) => tool.section === s.id));
  for (const s of present) {
    const button = document.createElement('button');
    button.type = 'button';
    button.textContent = s.name;
    button.dataset.section = String(s.id);
    button.addEventListener('click', () => {
      section = s.id;
      search.value = '';
      render();
    });
    sectionsNav.append(button);
  }

  search.addEventListener('input', () => render());
  search.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') { search.value = ''; render(); }
    if (event.key === 'Enter') grid.querySelector('.tool')?.click();
  });

  grid.addEventListener('click', (event) => {
    const button = event.target.closest('.tool');
    if (button) pick(0, button.dataset.id);
  });
  grid.addEventListener('contextmenu', (event) => {
    const button = event.target.closest('.tool');
    if (!button) return;
    event.preventDefault();
    pick(1, button.dataset.id);
  });
  grid.addEventListener('auxclick', (event) => {
    const button = event.target.closest('.tool');
    if (button && event.button === 1) pick(1, button.dataset.id);
  });

  const colour = root.querySelector('#paint-colour');
  const alpha = root.querySelector('#paint-alpha');
  const sendColour = () => {
    const value = Number.parseInt(colour.value.slice(1), 16);
    onPaintColour({ r: value >> 16, g: (value >> 8) & 255, b: value & 255, a: Number(alpha.value) });
  };
  colour.addEventListener('input', sendColour);
  alpha.addEventListener('input', sendColour);

  function matches(tool, words) {
    const text = `${tool.code} ${tool.label} ${tool.description}`.toLowerCase();
    return words.every((word) => text.includes(word));
  }

  function render() {
    const query = search.value.trim().toLowerCase();
    const words = query.split(/\s+/).filter(Boolean);
    // Hits in the code or name first, then those only in the description; the game's order within each.
    const rank = (tool) => (`${tool.code} ${tool.label}`.toLowerCase().includes(words[0]) ? 0 : 1);
    const shown = words.length
      ? tools.filter((tool) => matches(tool, words)).sort((a, b) => rank(a) - rank(b))
      : tools.filter((tool) => tool.section === section);
    for (const button of sectionsNav.children) button.setAttribute('aria-pressed', String(!words.length && Number(button.dataset.section) === section));
    paint.hidden = words.length > 0 || section !== PAINT;
    grid.replaceChildren(...shown.map(toolButton));
    if (!shown.length) {
      const empty = document.createElement('p');
      empty.className = 'empty';
      empty.textContent = `Nothing called “${query}”.`;
      grid.append(empty);
    }
    mark();
  }

  function toolButton(tool) {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'tool';
    button.dataset.id = tool.id;
    button.title = `${tool.label} (${tool.code}): ${tool.description}`;
    button.style.setProperty('--c', tool.colour);
    button.style.setProperty('--ink', inkFor(tool.colour));
    const code = document.createElement('span');
    code.className = 'code';
    code.textContent = tool.code;
    const name = document.createElement('span');
    name.className = 'name';
    name.textContent = tool.label;
    const badges = document.createElement('span');
    badges.className = 'badges';
    button.append(code, name, badges);
    return button;
  }

  function mark() {
    for (const button of grid.querySelectorAll('.tool')) {
      const left = button.dataset.id === picked[0];
      const right = button.dataset.id === picked[1];
      button.classList.toggle('left', left);
      button.classList.toggle('right', right);
      const badges = button.querySelector('.badges');
      const want = `${left ? 'L' : ''}${right ? 'R' : ''}`;
      if (badges.dataset.shown !== want) {
        badges.dataset.shown = want;
        badges.innerHTML = `${left ? '<i class="l">L</i>' : ''}${right ? '<i class="r">R</i>' : ''}`;
      }
    }
    picked.forEach((id, slot) => {
      const tool = byId.get(id);
      slots[slot].style.setProperty('--c', tool?.colour ?? '#333');
      slots[slot].querySelector('b').textContent = tool ? tool.label : id.replace(/^DEFAULT_[A-Z]+_/, '') || '…';
    });
  }

  render();
  return {
    /** What the game says each button draws with now (it changes them too: sampling, its own menus). */
    setPicked(ids) {
      if (ids[0] === picked[0] && ids[1] === picked[1]) return;
      picked = [ids[0] ?? '', ids[1] ?? ''];
      mark();
    },
    /** Show the section of a tool, e.g. after it was picked in the game. */
    reveal(id) {
      const tool = byId.get(id);
      if (!tool || search.value) return;
      if (tool.section !== section) { section = tool.section; render(); }
    },
    get eraser() { return byId.has(ERASER) ? ERASER : ''; },
  };
}