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 `