bit-twiddler 0.2.0

Cross-platform developer toolbox: bit manipulation, hashing, YAML/JSON/SQL, QR, Markdown, cron, and 40+ more tools — Tauri v2, no Node.js
  // --- Color Converter Logic ---
  const $colorInput = $('#color-input');
  const $colorError = $('#color-error');
  const $colorSwatch = $('#color-swatch');
  const $colorOutHex = $('#color-out-hex');
  const $colorOutRgb = $('#color-out-rgb');
  const $colorOutHsl = $('#color-out-hsl');
  const $colorOutCmyk = $('#color-out-cmyk');
  let lastScale = null;

  const hexToRgb = (hex) => {
      let r = 0, g = 0, b = 0;
      if (hex.length === 4) {
          r = "0x" + hex[1] + hex[1];
          g = "0x" + hex[2] + hex[2];
          b = "0x" + hex[3] + hex[3];
      } else if (hex.length === 7) {
          r = "0x" + hex[1] + hex[2];
          g = "0x" + hex[3] + hex[4];
          b = "0x" + hex[5] + hex[6];
      }
      return [Number(r), Number(g), Number(b)];
  };

  const rgbToHsl = (r, g, b) => {
      r /= 255; g /= 255; b /= 255;
      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
      if (max === min) {
          h = s = 0;
      } else {
          let d = max - min;
          s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
          switch(max) {
              case r: h = (g - b) / d + (g < b ? 6 : 0); break;
              case g: h = (b - r) / d + 2; break;
              case b: h = (r - g) / d + 4; break;
          }
          h /= 6;
      }
      return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
  };

  const rgbToCmyk = (r, g, b) => {
      let c = 1 - (r / 255);
      let m = 1 - (g / 255);
      let y = 1 - (b / 255);
      let k = Math.min(c, Math.min(m, y));
      if (k === 1) {
          return [0, 0, 0, 100];
      }
      c = Math.round((c - k) / (1 - k) * 100);
      m = Math.round((m - k) / (1 - k) * 100);
      y = Math.round((y - k) / (1 - k) * 100);
      k = Math.round(k * 100);
      return [c, m, y, k];
  };

  const hslToRgb = (h, s, l) => {
      h = ((h % 360) + 360) % 360;
      s /= 100; l /= 100;
      const c = (1 - Math.abs(2 * l - 1)) * s;
      const x = c * (1 - Math.abs((h / 60) % 2 - 1));
      const m = l - c / 2;
      let r1, g1, b1;
      if (h < 60)       [r1, g1, b1] = [c, x, 0];
      else if (h < 120) [r1, g1, b1] = [x, c, 0];
      else if (h < 180) [r1, g1, b1] = [0, c, x];
      else if (h < 240) [r1, g1, b1] = [0, x, c];
      else if (h < 300) [r1, g1, b1] = [x, 0, c];
      else              [r1, g1, b1] = [c, 0, x];
      return [
          Math.round((r1 + m) * 255),
          Math.round((g1 + m) * 255),
          Math.round((b1 + m) * 255)
      ];
  };

  const rgbToHex = (r, g, b) => '#' + [r, g, b]
      .map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0'))
      .join('').toUpperCase();

  const hslToHex = (h, s, l) => rgbToHex(...hslToRgb(h, s, l));

  const SCALE_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];

  const generateScale = (h, s) => {
      const n = SCALE_STEPS.length;
      return SCALE_STEPS.map((_, i) => hslToHex(h, s, Math.round(97 - i * ((97 - 8) / (n - 1)))));
  };

  const HARMONIES = [
      { name: 'Complementary', offsets: [0, 180] },
      { name: 'Split-Complementary', offsets: [0, 150, 210] },
      { name: 'Triadic', offsets: [0, 120, 240] },
      { name: 'Analogous', offsets: [-30, 0, 30] },
  ];

  const swatchHtml = (hex, labelTop, extraClass) => `
      <div class="color-swatch-btn cursor-pointer rounded-lg overflow-hidden border border-gray-800 hover:border-gray-600 transition-colors ${extraClass || ''}" data-copy="${hex}" title="Click to copy ${hex}">
        <div style="background:${hex}" class="h-10 w-full"></div>
        <div class="px-1 py-1 bg-gray-900 text-[8px] font-mono text-gray-500 text-center truncate">
          ${labelTop ? `<div class="text-gray-400 font-bold">${labelTop}</div>` : ''}
          <div>${hex}</div>
        </div>
      </div>`;

  const renderScale = (h, s) => {
      const scale = generateScale(h, s);
      $('#color-scale').html(scale.map((hex, i) => swatchHtml(hex, SCALE_STEPS[i])).join(''));
      return scale;
  };

  const renderHarmony = (h, s, l) => {
      $('#color-harmony').html(HARMONIES.map(({ name, offsets }) => `
        <div>
          <div class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-2">${name}</div>
          <div class="flex gap-1.5">
            ${offsets.map(off => swatchHtml(hslToHex(h + off, s, l), '', 'flex-1')).join('')}
          </div>
        </div>
      `).join(''));
  };

  const buildExport = (format, scale) => {
      const entries = SCALE_STEPS.map((step, i) => [step, scale[i]]);
      if (format === 'tailwind') {
          return 'module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        brand: {\n' +
              entries.map(([step, hex]) => `          ${step}: '${hex.toLowerCase()}',`).join('\n') +
              '\n        }\n      }\n    }\n  }\n}';
      }
      if (format === 'scss') {
          return '$palette: (\n' + entries.map(([step, hex]) => `  ${step}: ${hex.toLowerCase()},`).join('\n') + '\n);';
      }
      // css custom properties (default)
      return ':root {\n' + entries.map(([step, hex]) => `  --color-${step}: ${hex};`).join('\n') + '\n}';
  };

  const renderExport = (scale) => {
      $('#color-export-output').text(buildExport($('#color-export-format').val(), scale));
  };

  const clearScaleAndHarmony = () => {
      $('#color-scale').html('');
      $('#color-harmony').html('');
      $('#color-export-output').text('');
  };

  const clearColorUI = () => {
       $colorSwatch.css('background-color', '');
       $colorOutHex.text('');
       $colorOutRgb.text('');
       $colorOutHsl.text('');
       $colorOutCmyk.text('');
       clearScaleAndHarmony();
       lastScale = null;
  };

  $colorInput.on('input', function() {
      let val = $(this).val().trim();
      if (!val) {
          $colorError.addClass('hidden');
          clearColorUI();
          return;
      }
      if (!val.startsWith('#')) {
          val = '#' + val;
      }
      const validHex = /^#([0-9A-F]{3}){1,2}$/i.test(val);
      if (!validHex) {
          $colorError.removeClass('hidden');
          clearColorUI();
      } else {
          $colorError.addClass('hidden');
          const [r, g, b] = hexToRgb(val);
          const [h, s, l] = rgbToHsl(r, g, b);
          const [c, m, y, k] = rgbToCmyk(r, g, b);
          
          let fullHex = val.toUpperCase();
          if (fullHex.length === 4) {
              fullHex = '#' + fullHex[1]+fullHex[1]+fullHex[2]+fullHex[2]+fullHex[3]+fullHex[3];
          }

          $colorSwatch.css('background-color', fullHex);
          $colorOutHex.text(fullHex);
          $colorOutRgb.text(`rgb(${r}, ${g}, ${b})`);
          $colorOutHsl.text(`hsl(${h}, ${s}%, ${l}%)`);
          $colorOutCmyk.text(`cmyk(${c}%, ${m}%, ${y}%, ${k}%)`);

          lastScale = renderScale(h, s);
          renderHarmony(h, s, l);
          renderExport(lastScale);
      }
  });

  $('.color-copy-btn').on('click', function() {
    const text = $('#' + $(this).data('from')).text();
    if (text) window.copyToClipboard(text, $(this));
  });

  $(document).on('click', '.color-swatch-btn', function() {
    window.copyToClipboard($(this).data('copy').toString(), $(this).find('div').last());
  });

  $('#color-export-format').on('change', () => {
    if (lastScale) renderExport(lastScale);
  });

  $('#color-export-copy-btn').on('click', function() {
    const text = $('#color-export-output').text();
    if (text) window.copyToClipboard(text, $(this));
  });