LAUNCHPAD_MINI_BASIC_WEB_UI

Ausführen ZIP (lokal ausführbar)
Embed
<iframe src="https://code.turboflip.de/run/e17bd45c86ef0b9a/" sandbox="allow-scripts allow-forms allow-modals allow-downloads" loading="lazy" style="border:0;width:100%;height:480px"></iframe>
index.html
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LAUNCHPAD // Test</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: #fff; color: #111; font: 14px/1.4 system-ui, sans-serif; display: flex; justify-content: center; }
  main { width: 100%; max-width: 520px; padding: 32px 20px 48px; }
  h1 { font-size: 14px; font-weight: 600; letter-spacing: .12em; margin: 0 0 4px; }
  #status { color: #888; margin-bottom: 22px; min-height: 20px; }
  #status.ok { color: #111; }

  #pad { display: grid; grid-template-columns: repeat(9, 1fr); gap: 6px; margin-bottom: 22px; user-select: none; touch-action: none; }
  .b { aspect-ratio: 1; background: #f0f0f0; border: 1px solid #e2e2e2; border-radius: 6px; cursor: pointer; padding: 0; }
  .b.round { border-radius: 50%; }
  .b.none { visibility: hidden; }
  .b.down { outline: 2px solid #111; outline-offset: 1px; }

  .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; }
  .label { color: #888; width: 56px; }
  .sw { width: 28px; height: 28px; border-radius: 50%; border: 1px solid #ddd; cursor: pointer; padding: 0; background: #f0f0f0; }
  .sw.sel { outline: 2px solid #111; outline-offset: 2px; }
  button.t { font: inherit; background: #fff; border: 1px solid #ccc; border-radius: 6px; padding: 5px 12px; cursor: pointer; }
  button.t:hover { border-color: #111; }
  button.t.sel { background: #111; color: #fff; border-color: #111; }

  #log { margin: 18px 0 0; padding: 10px 12px; border: 1px solid #eee; border-radius: 6px; height: 170px; overflow: hidden;
         font: 12px/1.5 ui-monospace, Menlo, Consolas, monospace; color: #666; white-space: pre; }
</style>
</head>
<body>
<main>
  <h1>LAUNCHPAD MINI MK2</h1>
  <div id="status">Suche Gerät …</div>

  <div id="pad"></div>

  <div class="row">
    <span class="label">Farbe</span>
    <span id="palette" class="row" style="margin:0"></span>
  </div>
  <div class="row">
    <span class="label">Modus</span>
    <button class="t sel" data-mode="paint">Malen (Toggle)</button>
    <button class="t" data-mode="hold">Halten</button>
  </div>
  <div class="row">
    <span class="label"></span>
    <button class="t" id="btnClear">Alles aus</button>
    <button class="t" id="btnAll">Alles an</button>
    <button class="t" id="btnConnect">Neu verbinden</button>
  </div>

  <pre id="log">MIDI-Rohdaten erscheinen hier …</pre>
</main>

<script src="launchpad.js"></script>
<script>
(() => {
  const $ = (s) => document.querySelector(s);
  const pad = $('#pad'), statusEl = $('#status'), logEl = $('#log');
  const cells = {};          // "x,y" -> Element
  const virtDown = new Set();
  let lp = null, color = 'red', mode = 'paint';
  const lines = [];

  // ---- Darstellung ----
  const css = ([r, g]) => (r || g)
    ? `rgb(${r ? Math.min(255, 75 + r * 60) : 25},${g ? Math.min(255, 75 + g * 60) : 25},40)`
    : '#f0f0f0';

  // 9×9: Reihe -1 = obere Tasten, Spalte 8 = Seiten-Tasten
  for (let y = -1; y < 8; y++) {
    for (let x = 0; x < 9; x++) {
      const b = document.createElement('button');
      b.className = 'b' + ((y === -1 || x === 8) ? ' round' : '') + ((y === -1 && x === 8) ? ' none' : '');
      b.addEventListener('pointerdown', (e) => { e.preventDefault(); virtDown.add(x + ',' + y); press(x, y); render(); });
      const up = () => { if (virtDown.delete(x + ',' + y)) { release(x, y); render(); } };
      b.addEventListener('pointerup', up);
      b.addEventListener('pointerleave', up);
      pad.appendChild(b);
      cells[x + ',' + y] = b;
    }
  }

  function render() {
    for (const k in cells) {
      const [x, y] = k.split(',').map(Number);
      const c = cells[k];
      c.style.background = css(lp ? lp.getLed(x, y) : [0, 0]);
      c.classList.toggle('down', virtDown.has(k) || (lp && lp.isDown(x, y)));
    }
  }

  // ---- Palette / Modus ----
  const names = ['red', 'orange', 'amber', 'yellow', 'lime', 'green', 'off'];
  names.forEach((n) => {
    const s = document.createElement('button');
    s.className = 'sw' + (n === color ? ' sel' : '');
    s.style.background = css(Launchpad.COLORS[n]);
    s.title = n === 'off' ? 'Radierer' : n;
    s.onclick = () => {
      color = n;
      document.querySelectorAll('.sw').forEach((e) => e.classList.toggle('sel', e === s));
    };
    $('#palette').appendChild(s);
  });
  document.querySelectorAll('[data-mode]').forEach((b) => b.onclick = () => {
    mode = b.dataset.mode;
    document.querySelectorAll('[data-mode]').forEach((e) => e.classList.toggle('sel', e === b));
  });

  // ---- Logik: Tastendruck (Hardware oder Maus) färbt die LED ----
  function press(x, y) {
    if (!lp) return;
    if (mode === 'hold') lp.set(x, y, color);
    else {
      const cur = lp.getLed(x, y), want = Launchpad.COLORS[color];
      lp.set(x, y, (cur[0] === want[0] && cur[1] === want[1]) ? 'off' : color);
    }
  }
  function release(x, y) { if (lp && mode === 'hold') lp.set(x, y, 'off'); }

  $('#btnClear').onclick = () => { if (lp) lp.clear(); render(); };
  $('#btnAll').onclick = () => { if (lp) lp.fill(color === 'off' ? 'yellow' : color, { edges: true }); render(); };
  $('#btnConnect').onclick = init;

  // ---- Monitor ----
  const hex = (d) => d.map((v) => v.toString(16).padStart(2, '0')).join(' ');
  function log(text) {
    lines.push(text); if (lines.length > 10) lines.shift();
    logEl.textContent = lines.join('\n');
  }

  // ---- Verbinden ----
  function setStatus() {
    if (lp && lp.connected) { statusEl.textContent = 'Verbunden: ' + lp.deviceName; statusEl.className = 'ok'; }
    else { statusEl.textContent = 'Kein Launchpad gefunden – bitte anstecken (Maus-Pad funktioniert trotzdem).'; statusEl.className = ''; }
  }

  async function init() {
    try { lp = await Launchpad.connect(); }
    catch (e) { statusEl.textContent = e.message; statusEl.className = ''; return; }

    lp.on('press',   (e) => { log(`press   ${e.area.padEnd(4)} x=${e.x} y=${e.y}`); press(e.x, e.y);   render(); });
    lp.on('release', (e) => { log(`release ${e.area.padEnd(4)} x=${e.x} y=${e.y}`); release(e.x, e.y); render(); });
    lp.on('midi',    (e) => log('raw     ' + hex(e.data)));
    lp.on('led',     render);
    lp.on('clear',   render);
    lp.on('connect', setStatus);
    lp.on('disconnect', setStatus);
    setStatus(); render();
  }

  init();
})();
</script>
</body>
</html>
launchpad.js
/*
 * launchpad.js — Minimal-API für Novation Launchpad Mini MK2 (Web MIDI)
 *
 * Koordinaten (9×9-System):
 *   Raster       x 0–7, y 0–7   (y = 0 ist die oberste Reihe)
 *   obere Tasten x 0–7, y = -1  (area 'top')
 *   Seiten-Tasten x = 8, y 0–7  (area 'side')
 *
 * Farben: Name ('red', 'amber', 'off' …), [r, g] oder {r, g}, je 0–3.
 *
 * Nutzung:
 *   const lp = await Launchpad.connect();
 *   lp.on('press',   e => lp.set(e.x, e.y, 'red'));
 *   lp.on('release', e => lp.set(e.x, e.y, 'off'));
 */
(function (global) {
  'use strict';

  const COLORS = {
    off: [0, 0],
    'red-dim': [1, 0], red: [3, 0],
    orange: [3, 1], amber: [3, 2], yellow: [3, 3],
    lime: [1, 3], green: [0, 3], 'green-dim': [0, 1]
  };

  const clamp = (v) => Math.max(0, Math.min(3, v | 0));

  function normalize(c) {
    if (typeof c === 'string') {
      if (!(c in COLORS)) throw new Error('Unbekannte Farbe: ' + c);
      return COLORS[c].slice();
    }
    if (Array.isArray(c)) return [clamp(c[0]), clamp(c[1])];
    if (c && typeof c === 'object') return [clamp(c.r), clamp(c.g)];
    throw new Error('Ungültige Farbe');
  }

  // Velocity-Kodierung (Launchpad S / Mini): 16*Grün + Rot + 12 (copy + clear flags)
  const velocity = (rg) => 16 * rg[1] + rg[0] + 12;

  class Launchpad {
    constructor(access, opts) {
      this.access = access;
      this.matcher = opts.name instanceof RegExp ? opts.name
        : new RegExp(opts.name || 'launchpad', 'i');
      this.input = null;
      this.output = null;
      this.deviceName = null;
      this._h = {};
      this._down = new Set();
      this._leds = new Map();

      access.onstatechange = () => this._attach();
      this._attach();
    }

    static async connect(opts = {}) {
      if (!navigator.requestMIDIAccess) {
        throw new Error('Web MIDI wird von diesem Browser nicht unterstützt (Chrome, Edge oder Firefox ≥ 108 nutzen).');
      }
      const access = await navigator.requestMIDIAccess({ sysex: false });
      return new Launchpad(access, opts);
    }

    get connected() { return !!(this.input && this.output); }

    // ---------- Events ----------
    on(name, fn) { (this._h[name] = this._h[name] || []).push(fn); return this; }
    off(name, fn) {
      this._h[name] = (this._h[name] || []).filter((f) => f !== fn);
      return this;
    }
    _emit(name, payload) { (this._h[name] || []).forEach((f) => f(payload)); }

    // ---------- Ports ----------
    _attach() {
      const find = (map) => [...map.values()].find((p) => this.matcher.test(p.name) && p.state === 'connected');
      const input = find(this.access.inputs);
      const output = find(this.access.outputs);
      const was = this.connected;

      if (this.input && this.input !== input) this.input.onmidimessage = null;
      this.input = input || null;
      this.output = output || null;
      if (this.input) this.input.onmidimessage = (ev) => this._handle(ev);

      const now = this.connected;
      if (now && !was) {
        this.deviceName = this.input.name;
        this._leds.clear();
        this._send([0xB0, 0, 0]); // Reset: alle LEDs aus
        this._emit('connect', { name: this.deviceName });
      } else if (!now && was) {
        this._down.clear();
        this._emit('disconnect', { name: this.deviceName });
        this.deviceName = null;
      }
    }

    _send(bytes) { if (this.output) this.output.send(bytes); }

    // ---------- Eingabe ----------
    _handle(ev) {
      const data = Array.from(ev.data);
      this._emit('midi', { data });
      const [s, a, b] = data;
      const cmd = s & 0xF0;
      let x, y, area, on;

      if (cmd === 0x90 || cmd === 0x80) {
        const col = a & 15, row = a >> 4;
        if (col > 8 || row > 7) return;
        on = cmd === 0x90 && b > 0;
        if (col === 8) { x = 8; y = row; area = 'side'; }
        else { x = col; y = row; area = 'grid'; }
      } else if (cmd === 0xB0 && a >= 104 && a <= 111) {
        x = a - 104; y = -1; area = 'top'; on = b > 0;
      } else {
        return;
      }

      const key = x + ',' + y;
      if (on) this._down.add(key); else this._down.delete(key);
      this._emit(on ? 'press' : 'release', {
        x, y, area, index: area === 'side' ? y : area === 'top' ? x : null, velocity: b
      });
    }

    isDown(x, y) { return this._down.has(x + ',' + y); }

    // ---------- Ausgabe ----------
    set(x, y, color) {
      const rg = normalize(color);
      const v = velocity(rg);
      if (y === -1 && x >= 0 && x <= 7) this._send([0xB0, 104 + x, v]);
      else if (x === 8 && y >= 0 && y <= 7) this._send([0x90, y * 16 + 8, v]);
      else if (x >= 0 && x <= 7 && y >= 0 && y <= 7) this._send([0x90, y * 16 + x, v]);
      else throw new Error('Ungültige Position: ' + x + ',' + y);

      this._leds.set(x + ',' + y, rg);
      this._emit('led', { x, y, color: rg });
      return this;
    }

    setTop(i, color) { return this.set(i, -1, color); }
    setSide(i, color) { return this.set(8, i, color); }

    getLed(x, y) { return (this._leds.get(x + ',' + y) || [0, 0]).slice(); }

    // Raster füllen; mit {edges:true} auch obere und seitliche Tasten
    fill(color, opts = {}) {
      for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) this.set(x, y, color);
      if (opts.edges) for (let i = 0; i < 8; i++) { this.setTop(i, color); this.setSide(i, color); }
      return this;
    }

    // 64 Farben, zeilenweise (y*8 + x)
    frame(colors) {
      for (let i = 0; i < 64; i++) this.set(i % 8, (i / 8) | 0, colors[i] || 'off');
      return this;
    }

    clear() {
      this._send([0xB0, 0, 0]);
      this._leds.clear();
      this._emit('clear', {});
      return this;
    }
  }

  Launchpad.COLORS = COLORS;
  global.Launchpad = Launchpad;
})(window);