Created
August 22, 2026 20:50
-
-
Save monperrus/47e428a00451d2833221fe7efad86e41 to your computer and use it in GitHub Desktop.
Martin's Super Webapp tab: Piano
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "1": { | |
| "tab": { | |
| "id": "piano-tab", | |
| "label": "Piano", | |
| "html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n<title>Piano Tab - Jazz Manouche</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/vexflow/4.2.5/vexflow-min.js\"></script>\n<style>\n html, body { margin: 0; height: 100%; overflow: hidden; background: linear-gradient(135deg, #0b0f1a 0%, #0d1117 100%); color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }\n * { box-sizing: border-box; }\n .app { display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }\n .header { padding: 10px 16px 4px; text-align: center; }\n .header h1 { margin: 0; font-size: 16px; background: linear-gradient(135deg, #58a6ff, #79c0ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; }\n .header .sub { font-size: 11px; color: #8b949e; margin-top: 3px; letter-spacing: 0.3px; }\n .notation-area { flex: 1; min-height: 0; position: relative; background: rgba(22,27,34,0.6); margin: 6px 8px; border-radius: 12px; border: 1px solid #30363d; overflow: hidden; }\n #notation { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }\n #notation svg { max-width: 100%; max-height: 100%; }\n .controls { display: flex; gap: 8px; padding: 6px 12px; justify-content: center; align-items: center; flex-wrap: wrap; }\n .controls button { min-height: 46px; min-width: 46px; border: 1px solid #30363d; background: #21262d; color: #e6edf3; border-radius: 10px; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 18px; transition: all 0.15s; font-weight: 500; }\n .controls button:active { transform: scale(0.95); background: #30363d; }\n .controls button.primary { background: linear-gradient(135deg, #1f6feb, #58a6ff); border-color: transparent; color: #fff; box-shadow: 0 2px 8px rgba(31,111,235,0.3); }\n .controls .tempo { font-size: 12px; color: #8b949e; display: flex; align-items: center; gap: 6px; }\n .controls .tempo input { width: 60px; background: rgba(13,17,23,0.8); border: 1px solid #30363d; color: #e6edf3; border-radius: 6px; padding: 6px; font-size: 14px; text-align: center; height: 44px; }\n .piano-container { padding: 4px 8px 8px; overflow-x: auto; -webkit-overflow-scrolling: touch; }\n .piano { display: flex; position: relative; height: 120px; min-width: 700px; user-select: none; }\n .piano .key { cursor: pointer; position: relative; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 6px; font-size: 10px; color: #8b949e; transition: background 0.1s, border-color 0.1s; }\n .piano .key.white { width: 44px; height: 120px; background: linear-gradient(180deg, #f8f9fa, #e9ecef); border: 1px solid #dee2e6; border-radius: 0 0 6px 6px; color: #495057; z-index: 1; flex-shrink: 0; }\n .piano .key.white:active, .piano .key.white.active { background: linear-gradient(180deg, #58a6ff, #1f6feb); color: #fff; border-color: #1f6feb; }\n .piano .key.black { width: 28px; height: 72px; background: linear-gradient(180deg, #1a1a2e, #16213e); border: 1px solid #30363d; border-radius: 0 0 5px 5px; color: #8b949e; z-index: 2; margin-left: -14px; margin-right: -14px; flex-shrink: 0; }\n .piano .key.black:active, .piano .key.black.active { background: linear-gradient(180deg, #58a6ff, #1f6feb); color: #fff; }\n .piano .key.highlight { background: linear-gradient(180deg, #58a6ff, #1f6feb) !important; color: #fff !important; border-color: #1f6feb !important; }\n .note-indicator { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); padding: 6px 20px; border-radius: 20px; font-size: 16px; color: #fff; background: linear-gradient(135deg, #1f6feb, #58a6ff); font-weight: 700; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 10; box-shadow: 0 4px 12px rgba(31,111,235,0.3); }\n .note-indicator.show { opacity: 1; }\n .note-list { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 12px 8px; justify-content: center; }\n .note-list .note-chip { padding: 5px 12px; border-radius: 14px; font-size: 13px; background: rgba(22,27,34,0.6); border: 1px solid #30363d; color: #8b949e; font-weight: 500; transition: all 0.2s; }\n .note-list .note-chip.active { background: linear-gradient(135deg, #1f6feb, #58a6ff); color: #fff; border-color: transparent; box-shadow: 0 2px 8px rgba(31,111,235,0.3); }\n .note-list .note-chip.bar { border: none; background: none; color: #58a6ff; font-weight: 700; padding: 5px 2px; }\n @media (max-width: 480px) { .piano { height: 100px; min-width: 600px; } .piano .key.white { width: 38px; height: 100px; } .piano .key.black { width: 24px; height: 60px; margin-left: -12px; margin-right: -12px; } }\n</style>\n</head>\n<body>\n<div class=\"app\">\n <div class=\"header\">\n <h1>πΈ Jazz Manouche β’ Piano</h1>\n <div class=\"sub\">La β Do β Mi β Fa | Mi β RΓ© β Do β Si | La β Do β Mi β RΓ© | Do β Si β La β</div>\n </div>\n\n <div class=\"notation-area\">\n <div id=\"notation\"><div style=\"color:#8b949e;font-size:14px;text-align:center;padding:20px;\">πΌ Chargement...</div></div>\n <div class=\"note-indicator\" id=\"noteIndicator\"></div>\n </div>\n\n <div class=\"controls\">\n <button id=\"playBtn\" class=\"primary\">βΆ Play</button>\n <button id=\"resetBtn\">β² Reset</button>\n <div class=\"tempo\"><span>β©=</span><input type=\"number\" id=\"bpmInput\" value=\"180\" min=\"60\" max=\"300\"></div>\n </div>\n\n <div class=\"note-list\" id=\"noteList\"></div>\n\n <div class=\"piano-container\">\n <div class=\"piano\" id=\"piano\"></div>\n </div>\n</div>\n\n<script>\nconst NOTE_NAMES = ['Do', 'Do#', 'RΓ©', 'RΓ©#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'];\nconst melody = [\n { note: 'La', octave: 4, dur: 0.5, solfege: 'La' },\n { note: 'Do', octave: 5, dur: 0.5, solfege: 'Do' },\n { note: 'Mi', octave: 5, dur: 0.5, solfege: 'Mi' },\n { note: 'Fa', octave: 5, dur: 0.5, solfege: 'Fa' },\n { note: 'Mi', octave: 5, dur: 0.5, solfege: 'Mi' },\n { note: 'RΓ©', octave: 5, dur: 0.5, solfege: 'RΓ©' },\n { note: 'Do', octave: 5, dur: 0.5, solfege: 'Do' },\n { note: 'Si', octave: 4, dur: 0.5, solfege: 'Si' },\n { note: 'La', octave: 4, dur: 0.5, solfege: 'La' },\n { note: 'Do', octave: 5, dur: 0.5, solfege: 'Do' },\n { note: 'Mi', octave: 5, dur: 0.5, solfege: 'Mi' },\n { note: 'RΓ©', octave: 5, dur: 0.5, solfege: 'RΓ©' },\n { note: 'Do', octave: 5, dur: 0.5, solfege: 'Do' },\n { note: 'Si', octave: 4, dur: 0.5, solfege: 'Si' },\n { note: 'La', octave: 4, dur: 2.0, solfege: 'La' }\n];\nconst SOLFEGE_TO_MIDI = { 'Do': 0, 'Do#': 1, 'RΓ©': 2, 'RΓ©#': 3, 'Mi': 4, 'Fa': 5, 'Fa#': 6, 'Sol': 7, 'Sol#': 8, 'La': 9, 'La#': 10, 'Si': 11 };\nfunction noteToMidi(name, octave) { return (octave + 1) * 12 + SOLFEGE_TO_MIDI[name]; }\nfunction midiToFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }\nfunction noteToFreq(name, octave) { return midiToFreq(noteToMidi(name, octave)); }\n\nfunction buildPiano() {\n const container = document.getElementById('piano');\n const startNote = 48; const endNote = 84;\n const whiteKeys = []; const blackKeys = [];\n for (let midi = startNote; midi <= endNote; midi++) {\n const noteInOctave = midi % 12;\n const isBlack = [1, 3, 6, 8, 10].includes(noteInOctave);\n const octave = Math.floor(midi / 12) - 1;\n const name = NOTE_NAMES[noteInOctave];\n const label = isBlack ? '' : name + octave;\n const key = document.createElement('div');\n key.className = `key ${isBlack ? 'black' : 'white'}`;\n key.dataset.midi = midi; key.dataset.name = name; key.dataset.octave = octave;\n if (!isBlack) key.textContent = label;\n key.addEventListener('mousedown', (e) => { e.preventDefault(); playSingleNote(midi); });\n key.addEventListener('touchstart', (e) => { e.preventDefault(); playSingleNote(midi); });\n if (isBlack) blackKeys.push(key); else whiteKeys.push(key);\n }\n whiteKeys.forEach(k => container.appendChild(k));\n blackKeys.forEach(k => container.appendChild(k));\n}\n\nfunction buildNoteList() {\n const container = document.getElementById('noteList');\n melody.forEach((m, i) => {\n if (i === 4 || i === 8 || i === 12) { const bar = document.createElement('span'); bar.className = 'note-chip bar'; bar.textContent = '|'; container.appendChild(bar); }\n const chip = document.createElement('span'); chip.className = 'note-chip'; chip.textContent = m.solfege; chip.dataset.index = i; container.appendChild(chip);\n });\n}\n\nfunction renderNotation() {\n const div = document.getElementById('notation');\n if (typeof VexFlow === 'undefined') { div.innerHTML = '<div style=\"color:#8b949e;font-size:14px;text-align:center;padding:20px;\">πΌ Notation musicale<br><span style=\"font-size:12px;margin-top:8px;display:block;color:#58a6ff;\">La β Do β Mi β Fa | Mi β RΓ© β Do β Si | La β Do β Mi β RΓ© | Do β Si β La β</span></div>'; return; }\n try {\n const VF = VexFlow; div.innerHTML = '';\n const rect = div.getBoundingClientRect();\n const w = Math.max(rect.width, 300); const h = Math.max(rect.height, 140);\n const renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);\n renderer.resize(w, h); const context = renderer.getContext(); context.setFont('Arial', 10, '');\n const stave = new VF.Stave(10, 15, w - 20); stave.addClef('treble'); stave.addTimeSignature('4/4'); stave.setContext(context).draw();\n const SOLFEGE_TO_VEX = { 'Do': 'C', 'RΓ©': 'D', 'Mi': 'E', 'Fa': 'F', 'Sol': 'G', 'La': 'A', 'Si': 'B' };\n const vfNotes = melody.map((n, i) => { const vexName = SOLFEGE_TO_VEX[n.solfege]; const octave = n.solfege === 'La' || n.solfege === 'Si' ? 4 : 5; const isLast = (i === melody.length - 1); const dur = isLast ? 'h' : '8'; return new VF.StaveNote({ clef: 'treble', keys: [vexName + '/' + octave], duration: dur }); });\n const beams = [];\n for (let g = 0; g < 4; g++) { const group = vfNotes.slice(g * 4, g === 3 ? 14 : g * 4 + 4); if (group.length > 1) { try { beams.push(new VF.Beam(group)); } catch(e) {} } }\n try { const voice = new VF.Voice({ num_beats: 4, beat_value: 4 }); voice.addTickables(vfNotes); new VF.Formatter().joinVoices([voice]).format([voice], w - 60); voice.draw(context, stave); beams.forEach(b => { try { b.setContext(context).draw(); } catch(e) {} }); } catch(e) { div.innerHTML = '<div style=\"color:#8b949e;font-size:14px;text-align:center;padding:20px;\">πΌ La β Do β Mi β Fa | Mi β RΓ© β Do β Si | La β Do β Mi β RΓ© | Do β Si β La β</div>'; }\n } catch(e) { div.innerHTML = '<div style=\"color:#8b949e;font-size:14px;text-align:center;padding:20px;\">πΌ La β Do β Mi β Fa | Mi β RΓ© β Do β Si | La β Do β Mi β RΓ© | Do β Si β La β</div>'; }\n}\n\nlet audioCtx = null; let isPlaying = false; let currentNoteIndex = -1; let timeoutIds = []; let bpm = 180;\nfunction getAudioContext() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (audioCtx.state === 'suspended') audioCtx.resume(); return audioCtx; }\nfunction playSingleNote(midi) {\n const ctx = getAudioContext(); const freq = midiToFreq(midi); const now = ctx.currentTime;\n const osc = ctx.createOscillator(); const gain = ctx.createGain(); const filter = ctx.createBiquadFilter();\n osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, now);\n filter.type = 'lowpass'; filter.frequency.setValueAtTime(2000, now); filter.frequency.exponentialRampToValueAtTime(800, now + 0.5);\n gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.8);\n osc.connect(filter); filter.connect(gain); gain.connect(ctx.destination); osc.start(now); osc.stop(now + 0.8);\n const keys = document.querySelectorAll('.piano .key');\n keys.forEach(k => { k.classList.remove('highlight'); if (parseInt(k.dataset.midi) === midi) k.classList.add('highlight'); });\n setTimeout(() => { keys.forEach(k => k.classList.remove('highlight')); }, 400);\n}\n\nfunction playMelodyNote(noteObj, startTime, noteIdx) {\n const ctx = getAudioContext(); const freq = noteToFreq(noteObj.note, noteObj.octave); const beatDur = 60 / bpm; const dur = noteObj.dur * 2 * beatDur;\n const osc = ctx.createOscillator(); const gain = ctx.createGain(); const filter = ctx.createBiquadFilter();\n osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, startTime);\n const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(freq * 2, startTime); gain2.gain.setValueAtTime(0.03, startTime); gain2.gain.exponentialRampToValueAtTime(0.001, startTime + dur * 0.7);\n filter.type = 'lowpass'; filter.frequency.setValueAtTime(2500, startTime); filter.frequency.exponentialRampToValueAtTime(600, startTime + dur);\n gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(0.18, startTime + 0.01); gain.gain.setValueAtTime(0.16, startTime + dur * 0.7); gain.gain.exponentialRampToValueAtTime(0.001, startTime + dur);\n osc.connect(filter); filter.connect(gain); gain.connect(ctx.destination); osc2.connect(gain2); gain2.connect(ctx.destination);\n osc.start(startTime); osc.stop(startTime + dur + 0.05); osc2.start(startTime); osc2.stop(startTime + dur + 0.05);\n const midi = noteToMidi(noteObj.note, noteObj.octave); const keys = document.querySelectorAll('.piano .key'); keys.forEach(k => k.classList.remove('active'));\n const keyEl = Array.from(keys).find(k => parseInt(k.dataset.midi) === midi); if (keyEl) keyEl.classList.add('active');\n const chips = document.querySelectorAll('.note-chip:not(.bar)'); chips.forEach(c => c.classList.remove('active')); if (noteIdx !== undefined && chips[noteIdx]) chips[noteIdx].classList.add('active');\n const indicator = document.getElementById('noteIndicator'); indicator.textContent = 'βͺ ' + noteObj.solfege; indicator.classList.add('show');\n const cleanupId = setTimeout(() => { if (keyEl) keyEl.classList.remove('active'); }, dur * 1000); timeoutIds.push(cleanupId);\n}\n\nfunction playMelody() {\n if (isPlaying) return; isPlaying = true; currentNoteIndex = 0;\n const btn = document.getElementById('playBtn'); btn.textContent = 'βΈ Stop'; btn.classList.remove('primary');\n const ctx = getAudioContext(); const beatDur = 60 / bpm; let startTime = ctx.currentTime + 0.15;\n melody.forEach((noteObj, i) => {\n const dur = noteObj.dur * 2 * beatDur; const t = startTime;\n const id = setTimeout(() => { playMelodyNote(noteObj, ctx.currentTime, i); currentNoteIndex = i; if (i === melody.length - 1) setTimeout(() => stopMelody(), 600); }, (t - ctx.currentTime) * 1000);\n timeoutIds.push(id); startTime += dur;\n });\n}\n\nfunction stopMelody() {\n isPlaying = false; currentNoteIndex = -1; timeoutIds.forEach(id => clearTimeout(id)); timeoutIds = [];\n document.querySelectorAll('.piano .key').forEach(k => k.classList.remove('active')); document.querySelectorAll('.note-chip:not(.bar)').forEach(c => c.classList.remove('active')); document.getElementById('noteIndicator').classList.remove('show');\n const btn = document.getElementById('playBtn'); btn.textContent = 'βΆ Play'; btn.classList.add('primary');\n}\n\ndocument.getElementById('playBtn').addEventListener('click', () => { if (isPlaying) stopMelody(); else playMelody(); });\ndocument.getElementById('resetBtn').addEventListener('click', () => { stopMelody(); document.querySelectorAll('.piano .key').forEach(k => k.classList.remove('highlight')); });\ndocument.getElementById('bpmInput').addEventListener('change', (e) => { bpm = parseInt(e.target.value) || 180; if (bpm < 60) bpm = 60; if (bpm > 300) bpm = 300; });\n\nbuildPiano(); buildNoteList();\nsetTimeout(renderNotation, 100);\nlet resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(renderNotation, 300); });\n</script>\n</body>\n</html>" | |
| }, | |
| "currentHash": "f0cd48f363ab5fe6ea002b34f1e115ff6172bf850f519b481eaf59c40b789001", | |
| "previousHash": null, | |
| "prompt": "Published shared tab" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment