From 2cf1ceeef917527578c5b65825575ba4cbbcb5b6 Mon Sep 17 00:00:00 2001 From: Alexander Rossmanith Date: Sun, 26 Jul 2026 15:53:03 +0530 Subject: [PATCH] move css and js files into folders --- styles.css => css/styles.css | 300 +++++++++++++-------------- script.js => js/script.js | 386 +++++++++++++++++------------------ 2 files changed, 343 insertions(+), 343 deletions(-) rename styles.css => css/styles.css (95%) rename script.js => js/script.js (96%) diff --git a/styles.css b/css/styles.css similarity index 95% rename from styles.css rename to css/styles.css index 89ad44a..9cddd67 100644 --- a/styles.css +++ b/css/styles.css @@ -1,150 +1,150 @@ -body { - font-family: sans-serif; - text-align: center; - background: #2a2a2a; - padding: 20px; -} - -h1 { - color: #fff; -} - -.controls { - margin-bottom: 30px; -} - -.controls label { - color: #fff; - margin-right: 10px; - font-size: 16px; -} - -.controls select { - padding: 8px 12px; - font-size: 14px; - border-radius: 5px; - border: none; - background: #fff; - color: #333; - cursor: pointer; -} - -.piano { - display: flex; - justify-content: center; - align-items: flex-end; - margin-top: 50px; - background: #111; - padding: 20px; - border-radius: 10px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); -} - -.key { - display: flex; - align-items: flex-end; - justify-content: center; - font-size: 14px; - cursor: pointer; - border: 1px solid #222; - border-radius: 5px; - user-select: none; - transition: all 0.05s; - position: relative; - min-height: 1em; -} - -/* Sharp keys may show Carnatic swara labels if they correspond to the chosen scale */ -.key.sharp.sa-root { - color: #fff; - font-size: 14px; -} - -.key.has-label .key-label { - position: relative; - display: inline-flex; -} - -.key.has-label .key-label sub { - font-size: 0.65em; - display: inline-block; - vertical-align: text-bottom; - line-height: 1; - position: relative; - top: 0.18em; -} - -.key.lower.has-label .key-label::after, -.key.upper.has-label .key-label::after { - position: absolute; - content: '•'; - font-size: 12px; - color: currentColor; - pointer-events: none; - left: 50%; - transform: translateX(-50%); -} - -.key.black.has-label .key-label::after { - color: #777; -} - -.key.lower.has-label .key-label::after { - top: 100%; - margin-top: -2px; -} - -.key.upper.has-label .key-label::after { - bottom: 100%; - margin-bottom: -5px; -} - -/* White keys */ -.key.white { - width: 60px; - height: 250px; - background: linear-gradient(135deg, #fff 0%, #f5f5f5 100%); - color: #333; - margin: 0 -1px; - padding-bottom: 15px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); -} - -.key.white:active { - background: linear-gradient(135deg, #e0e0e0 0%, #d5d5d5 100%); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), inset 0 1px 3px rgba(0, 0, 0, 0.2); - transform: translateY(2px); -} - -/* Black keys */ -.key.black { - width: 40px; - height: 160px; - background: linear-gradient(135deg, #222 0%, #111 100%); - color: #fff; - position: relative; - z-index: 10; - margin: 0 -20px; - padding-top: 10px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); - align-self: flex-start; -} - -.key.black:active { - background: linear-gradient(135deg, #111 0%, #000 100%); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), inset 0 1px 2px rgba(255, 255, 255, 0.05); - transform: translateY(2px); -} - -.key.sa-root.white { - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); -} - -.key.sa-root.black { - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); -} - -.key.sa-root:not(.sharp) { - color: #d84315; - font-weight: 700; -} +body { + font-family: sans-serif; + text-align: center; + background: #2a2a2a; + padding: 20px; +} + +h1 { + color: #fff; +} + +.controls { + margin-bottom: 30px; +} + +.controls label { + color: #fff; + margin-right: 10px; + font-size: 16px; +} + +.controls select { + padding: 8px 12px; + font-size: 14px; + border-radius: 5px; + border: none; + background: #fff; + color: #333; + cursor: pointer; +} + +.piano { + display: flex; + justify-content: center; + align-items: flex-end; + margin-top: 50px; + background: #111; + padding: 20px; + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); +} + +.key { + display: flex; + align-items: flex-end; + justify-content: center; + font-size: 14px; + cursor: pointer; + border: 1px solid #222; + border-radius: 5px; + user-select: none; + transition: all 0.05s; + position: relative; + min-height: 1em; +} + +/* Sharp keys may show Carnatic swara labels if they correspond to the chosen scale */ +.key.sharp.sa-root { + color: #fff; + font-size: 14px; +} + +.key.has-label .key-label { + position: relative; + display: inline-flex; +} + +.key.has-label .key-label sub { + font-size: 0.65em; + display: inline-block; + vertical-align: text-bottom; + line-height: 1; + position: relative; + top: 0.18em; +} + +.key.lower.has-label .key-label::after, +.key.upper.has-label .key-label::after { + position: absolute; + content: '•'; + font-size: 12px; + color: currentColor; + pointer-events: none; + left: 50%; + transform: translateX(-50%); +} + +.key.black.has-label .key-label::after { + color: #777; +} + +.key.lower.has-label .key-label::after { + top: 100%; + margin-top: -2px; +} + +.key.upper.has-label .key-label::after { + bottom: 100%; + margin-bottom: -5px; +} + +/* White keys */ +.key.white { + width: 60px; + height: 250px; + background: linear-gradient(135deg, #fff 0%, #f5f5f5 100%); + color: #333; + margin: 0 -1px; + padding-bottom: 15px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); +} + +.key.white:active { + background: linear-gradient(135deg, #e0e0e0 0%, #d5d5d5 100%); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), inset 0 1px 3px rgba(0, 0, 0, 0.2); + transform: translateY(2px); +} + +/* Black keys */ +.key.black { + width: 40px; + height: 160px; + background: linear-gradient(135deg, #222 0%, #111 100%); + color: #fff; + position: relative; + z-index: 10; + margin: 0 -20px; + padding-top: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); + align-self: flex-start; +} + +.key.black:active { + background: linear-gradient(135deg, #111 0%, #000 100%); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), inset 0 1px 2px rgba(255, 255, 255, 0.05); + transform: translateY(2px); +} + +.key.sa-root.white { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); +} + +.key.sa-root.black { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); +} + +.key.sa-root:not(.sharp) { + color: #d84315; + font-weight: 700; +} diff --git a/script.js b/js/script.js similarity index 96% rename from script.js rename to js/script.js index b223d52..31f0bbf 100644 --- a/script.js +++ b/js/script.js @@ -1,193 +1,193 @@ -// Initialize audio context lazily to avoid autoplay restrictions -let audioCtx; - -const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; - -const SVARA_NAMES = { - full: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], - short: ["S", "R", "G", "M", "P", "D", "N"], - malayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"] -}; - -const semitoneMap = { - C: 0, - "C#": 1, - D: 2, - "D#": 3, - E: 4, - F: 5, - "F#": 6, - G: 7, - "G#": 8, - A: 9, - "A#": 10, - B: 11, - Ab: 8, - Bb: 10 -}; -const ragams = { - Bhair: { - intervals: [0, 2, 3, 6, 7, 9, 11], - namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], - namesShort: ["S", "R", "G", "M", "P", "D", "N"], - namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], - variantNumbers: ["", "2", "2", "1", "", "2", "2"] - }, - Kalyani: { - intervals: [0, 2, 4, 6, 7, 9, 11], - namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], - namesShort: ["S", "R", "G", "M", "P", "D", "N"], - namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], - variantNumbers: ["", "2", "3", "2", "", "2", "3"] - }, - Mayamalavagowla: { - intervals: [0, 1, 4, 5, 7, 8, 11], - namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], - namesShort: ["S", "R", "G", "M", "P", "D", "N"], - namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], - variantNumbers: ["", "1", "3", "1", "", "1", "3"] - }, - Mohanam: { - intervals: [0, 2, 4, 7, 9], - namesFull: ["Sa", "Ri", "Ga", "Pa", "Dha"], - namesShort: ["S", "R", "G", "P", "D"], - namesMalayalam: ["സ", "രി", "ഗ", "പ", "ധ"], - variantNumbers: ["", "2", "3", "", "2"] - }, - Shankarabharanam: { - intervals: [0, 2, 4, 5, 7, 9, 11], - namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], - namesShort: ["S", "R", "G", "M", "P", "D", "N"], - namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], - variantNumbers: ["", "2", "3", "1", "", "2", "3"] - } -}; -const ROOT_OCTAVE = 4; -const SOUND_OCTAVE_OFFSET = -12; - -const svaraNamesSelect = document.getElementById("svaraNames"); -const svaraStyleSelect = document.getElementById("svaraStyle"); -const startingNoteSelect = document.getElementById("startingNote"); -const ragamSelect = document.getElementById("ragamSelect"); -const pianoElement = document.getElementById("piano"); - -function normalizeSemitone(value) { - return ((value % 12) + 12) % 12; -} - -function absFrequency(absSemitone) { - const a4Abs = 4 * 12 + semitoneMap.A; // fixed A4 reference for real pitch - const soundAbsSemitone = absSemitone + SOUND_OCTAVE_OFFSET; - return 440 * Math.pow(2, (soundAbsSemitone - a4Abs) / 12); -} - -function noteNameToAbs(noteName) { - return ROOT_OCTAVE * 12 + semitoneMap[noteName]; -} - -function getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { - const diff = normalizeSemitone(absSemitone - startAbs); - const intervalIndex = ragam.intervals.indexOf(diff); - - if (intervalIndex === -1) { - return ""; - } - - const baseLabel = svaraNames[intervalIndex]; - if (svaraStyle === "variant" && ragam.variantNumbers) { - const variant = ragam.variantNumbers[intervalIndex]; - if (variant) { - return `${baseLabel}${variant}`; - } - } - - return baseLabel; -} - -function buildKeyHtml(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { - const noteName = NOTE_NAMES[normalizeSemitone(absSemitone)]; - const isSharp = noteName.includes("#"); - const label = getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle); - const classes = ["key", isSharp ? "black" : "white"]; - - if (absSemitone < startAbs) { - classes.push("lower"); - } else if (absSemitone > startAbs + 11) { - classes.push("upper"); - } - - if (normalizeSemitone(absSemitone - startAbs) === 0) { - classes.push("sa-root"); - } - - if (label) { - classes.push("has-label"); - } - - const content = label ? `${label}` : ""; - return `
${content}
`; -} - -function renderKeyboard() { - const startNote = startingNoteSelect.value; - const svaraNames = SVARA_NAMES[svaraNamesSelect.value] || SVARA_NAMES.full; - const svaraStyle = svaraStyleSelect.value; - const ragam = ragams[ragamSelect.value]; - const startAbs = noteNameToAbs(startNote); - const lowerPaAbs = startAbs - 5; - const upperPaAbs = startAbs + 19; - let html = ""; - - for (let abs = lowerPaAbs; abs <= upperPaAbs; abs += 1) { - html += buildKeyHtml(abs, startAbs, svaraNames, ragam, svaraStyle); - } - - pianoElement.innerHTML = html; - attachKeyListeners(); -} - -function attachKeyListeners() { - const keys = pianoElement.querySelectorAll(".key"); - keys.forEach((key) => { - key.addEventListener("click", () => { - playNote(parseInt(key.dataset.abs, 10)); - }); - }); -} - -function ensureAudioContext() { - if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - return audioCtx; -} - -async function playNote(absSemitone) { - const ctx = ensureAudioContext(); - if (ctx.state === "suspended") { - await ctx.resume(); - } - - const freq = absFrequency(absSemitone); - - const oscillator = ctx.createOscillator(); - const gainNode = ctx.createGain(); - - oscillator.type = "sine"; - oscillator.frequency.setValueAtTime(freq, ctx.currentTime); - - oscillator.connect(gainNode); - gainNode.connect(ctx.destination); - - gainNode.gain.setValueAtTime(0.2, ctx.currentTime); - oscillator.start(); - oscillator.stop(ctx.currentTime + 0.5); -} - -renderKeyboard(); -svaraNamesSelect.addEventListener("change", renderKeyboard); -svaraStyleSelect.addEventListener("change", renderKeyboard); -startingNoteSelect.addEventListener("change", renderKeyboard); -ragamSelect.addEventListener("change", renderKeyboard); - - +// Initialize audio context lazily to avoid autoplay restrictions +let audioCtx; + +const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; + +const SVARA_NAMES = { + full: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], + short: ["S", "R", "G", "M", "P", "D", "N"], + malayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"] +}; + +const semitoneMap = { + C: 0, + "C#": 1, + D: 2, + "D#": 3, + E: 4, + F: 5, + "F#": 6, + G: 7, + "G#": 8, + A: 9, + "A#": 10, + B: 11, + Ab: 8, + Bb: 10 +}; +const ragams = { + Bhair: { + intervals: [0, 2, 3, 6, 7, 9, 11], + namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], + namesShort: ["S", "R", "G", "M", "P", "D", "N"], + namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], + variantNumbers: ["", "2", "2", "1", "", "2", "2"] + }, + Kalyani: { + intervals: [0, 2, 4, 6, 7, 9, 11], + namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], + namesShort: ["S", "R", "G", "M", "P", "D", "N"], + namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], + variantNumbers: ["", "2", "3", "2", "", "2", "3"] + }, + Mayamalavagowla: { + intervals: [0, 1, 4, 5, 7, 8, 11], + namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], + namesShort: ["S", "R", "G", "M", "P", "D", "N"], + namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], + variantNumbers: ["", "1", "3", "1", "", "1", "3"] + }, + Mohanam: { + intervals: [0, 2, 4, 7, 9], + namesFull: ["Sa", "Ri", "Ga", "Pa", "Dha"], + namesShort: ["S", "R", "G", "P", "D"], + namesMalayalam: ["സ", "രി", "ഗ", "പ", "ധ"], + variantNumbers: ["", "2", "3", "", "2"] + }, + Shankarabharanam: { + intervals: [0, 2, 4, 5, 7, 9, 11], + namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], + namesShort: ["S", "R", "G", "M", "P", "D", "N"], + namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], + variantNumbers: ["", "2", "3", "1", "", "2", "3"] + } +}; +const ROOT_OCTAVE = 4; +const SOUND_OCTAVE_OFFSET = -12; + +const svaraNamesSelect = document.getElementById("svaraNames"); +const svaraStyleSelect = document.getElementById("svaraStyle"); +const startingNoteSelect = document.getElementById("startingNote"); +const ragamSelect = document.getElementById("ragamSelect"); +const pianoElement = document.getElementById("piano"); + +function normalizeSemitone(value) { + return ((value % 12) + 12) % 12; +} + +function absFrequency(absSemitone) { + const a4Abs = 4 * 12 + semitoneMap.A; // fixed A4 reference for real pitch + const soundAbsSemitone = absSemitone + SOUND_OCTAVE_OFFSET; + return 440 * Math.pow(2, (soundAbsSemitone - a4Abs) / 12); +} + +function noteNameToAbs(noteName) { + return ROOT_OCTAVE * 12 + semitoneMap[noteName]; +} + +function getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { + const diff = normalizeSemitone(absSemitone - startAbs); + const intervalIndex = ragam.intervals.indexOf(diff); + + if (intervalIndex === -1) { + return ""; + } + + const baseLabel = svaraNames[intervalIndex]; + if (svaraStyle === "variant" && ragam.variantNumbers) { + const variant = ragam.variantNumbers[intervalIndex]; + if (variant) { + return `${baseLabel}${variant}`; + } + } + + return baseLabel; +} + +function buildKeyHtml(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { + const noteName = NOTE_NAMES[normalizeSemitone(absSemitone)]; + const isSharp = noteName.includes("#"); + const label = getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle); + const classes = ["key", isSharp ? "black" : "white"]; + + if (absSemitone < startAbs) { + classes.push("lower"); + } else if (absSemitone > startAbs + 11) { + classes.push("upper"); + } + + if (normalizeSemitone(absSemitone - startAbs) === 0) { + classes.push("sa-root"); + } + + if (label) { + classes.push("has-label"); + } + + const content = label ? `${label}` : ""; + return `
${content}
`; +} + +function renderKeyboard() { + const startNote = startingNoteSelect.value; + const svaraNames = SVARA_NAMES[svaraNamesSelect.value] || SVARA_NAMES.full; + const svaraStyle = svaraStyleSelect.value; + const ragam = ragams[ragamSelect.value]; + const startAbs = noteNameToAbs(startNote); + const lowerPaAbs = startAbs - 5; + const upperPaAbs = startAbs + 19; + let html = ""; + + for (let abs = lowerPaAbs; abs <= upperPaAbs; abs += 1) { + html += buildKeyHtml(abs, startAbs, svaraNames, ragam, svaraStyle); + } + + pianoElement.innerHTML = html; + attachKeyListeners(); +} + +function attachKeyListeners() { + const keys = pianoElement.querySelectorAll(".key"); + keys.forEach((key) => { + key.addEventListener("click", () => { + playNote(parseInt(key.dataset.abs, 10)); + }); + }); +} + +function ensureAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioCtx; +} + +async function playNote(absSemitone) { + const ctx = ensureAudioContext(); + if (ctx.state === "suspended") { + await ctx.resume(); + } + + const freq = absFrequency(absSemitone); + + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + + oscillator.type = "sine"; + oscillator.frequency.setValueAtTime(freq, ctx.currentTime); + + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + + gainNode.gain.setValueAtTime(0.2, ctx.currentTime); + oscillator.start(); + oscillator.stop(ctx.currentTime + 0.5); +} + +renderKeyboard(); +svaraNamesSelect.addEventListener("change", renderKeyboard); +svaraStyleSelect.addEventListener("change", renderKeyboard); +startingNoteSelect.addEventListener("change", renderKeyboard); +ragamSelect.addEventListener("change", renderKeyboard); + +