diff --git a/frontend/src/melodies/BinaryTableModal.jsx b/frontend/src/melodies/BinaryTableModal.jsx index 15a648d..0bce158 100644 --- a/frontend/src/melodies/BinaryTableModal.jsx +++ b/frontend/src/melodies/BinaryTableModal.jsx @@ -18,6 +18,39 @@ function parseStepsString(stepsStr) { return String(stepsStr).trim().split(",").map((s) => parseBellNotation(s)); } +function interpolateHue(t) { + const stops = [ + [0.0, 190], + [0.24, 140], + [0.5, 56], + [0.82, 30], + [1.0, 0], + ]; + for (let i = 0; i < stops.length - 1; i++) { + const [aPos, aHue] = stops[i]; + const [bPos, bHue] = stops[i + 1]; + if (t >= aPos && t <= bPos) { + const local = (t - aPos) / (bPos - aPos || 1); + return aHue + (bHue - aHue) * local; + } + } + return stops[stops.length - 1][1]; +} + +function noteDotColor(noteNumber) { + const n = Number(noteNumber || 1); + const t = Math.min(1, Math.max(0, (n - 1) / 15)); + const hue = interpolateHue(t); + return `hsl(${hue}, 78%, 68%)`; +} + +function noteDotGlow(noteNumber) { + const n = Number(noteNumber || 1); + const t = Math.min(1, Math.max(0, (n - 1) / 15)); + const hue = interpolateHue(t); + return `hsla(${hue}, 78%, 56%, 0.45)`; +} + function normalizeFileUrl(url) { if (!url || typeof url !== "string") return null; if (url.startsWith("http") || url.startsWith("/api")) return url; @@ -193,10 +226,10 @@ export default function BinaryTableModal({ open, melody, builtMelody, files, arc width: "60%", height: "60%", borderRadius: "9999px", - backgroundColor: "var(--btn-primary)", + backgroundColor: noteDotColor(noteIdx + 1), opacity: enabled ? 1 : 0, transform: enabled ? "scale(1)" : "scale(0.4)", - boxShadow: enabled ? "0 0 10px 3px rgba(116, 184, 22, 0.5)" : "none", + boxShadow: enabled ? `0 0 10px 3px ${noteDotGlow(noteIdx + 1)}` : "none", transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease", }} /> @@ -225,4 +258,3 @@ export default function BinaryTableModal({ open, melody, builtMelody, files, arc ); } - diff --git a/frontend/src/melodies/MelodyComposer.jsx b/frontend/src/melodies/MelodyComposer.jsx index 6965400..4c8b5c0 100644 --- a/frontend/src/melodies/MelodyComposer.jsx +++ b/frontend/src/melodies/MelodyComposer.jsx @@ -22,6 +22,39 @@ function stepToHex(stepValue) { return `0x${(stepValue >>> 0).toString(16).toUpperCase().padStart(4, "0")}`; } +function interpolateHue(t) { + const stops = [ + [0.0, 190], + [0.24, 140], + [0.5, 56], + [0.82, 30], + [1.0, 0], + ]; + for (let i = 0; i < stops.length - 1; i++) { + const [aPos, aHue] = stops[i]; + const [bPos, bHue] = stops[i + 1]; + if (t >= aPos && t <= bPos) { + const local = (t - aPos) / (bPos - aPos || 1); + return aHue + (bHue - aHue) * local; + } + } + return stops[stops.length - 1][1]; +} + +function noteDotColor(noteNumber) { + const n = Number(noteNumber || 1); + const t = Math.min(1, Math.max(0, (n - 1) / 15)); + const hue = interpolateHue(t); + return `hsl(${hue}, 78%, 68%)`; +} + +function noteDotGlow(noteNumber) { + const n = Number(noteNumber || 1); + const t = Math.min(1, Math.max(0, (n - 1) / 15)); + const hue = interpolateHue(t); + return `hsla(${hue}, 78%, 56%, 0.45)`; +} + function playStep(audioCtx, stepValue, noteDurationMs) { if (!audioCtx) return; @@ -527,10 +560,10 @@ export default function MelodyComposer() { width: "54%", height: "54%", borderRadius: "9999px", - backgroundColor: "var(--btn-primary)", + backgroundColor: noteDotColor(noteIndex + 1), opacity: enabled ? 1 : 0, transform: enabled ? "scale(1)" : "scale(0.4)", - boxShadow: enabled ? "0 0 10px 3px rgba(116, 184, 22, 0.5)" : "none", + boxShadow: enabled ? `0 0 10px 3px ${noteDotGlow(noteIndex + 1)}` : "none", transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease", }} /> diff --git a/frontend/src/melodies/PlaybackModal.jsx b/frontend/src/melodies/PlaybackModal.jsx index 22c9b66..046c7ec 100644 --- a/frontend/src/melodies/PlaybackModal.jsx +++ b/frontend/src/melodies/PlaybackModal.jsx @@ -110,12 +110,39 @@ function applyNoteAssignments(rawStepValue, noteAssignments) { return result; } +function interpolateHue(t) { + const stops = [ + [0.0, 190], // bright teal/blue + [0.24, 140], // green + [0.5, 56], // yellow + [0.82, 30], // orange + [1.0, 0], // red + ]; + for (let i = 0; i < stops.length - 1; i++) { + const [aPos, aHue] = stops[i]; + const [bPos, bHue] = stops[i + 1]; + if (t >= aPos && t <= bPos) { + const local = (t - aPos) / (bPos - aPos || 1); + return aHue + (bHue - aHue) * local; + } + } + return stops[stops.length - 1][1]; +} + function bellDotColor(assignedBell) { const bell = Number(assignedBell || 0); if (bell <= 0) return "rgba(148,163,184,0.7)"; const t = Math.min(1, Math.max(0, (bell - 1) / 15)); - const light = 62 - t * 40; // bright green -> very dark green - return `hsl(132, 74%, ${light}%)`; + const hue = interpolateHue(t); + return `hsl(${hue}, 78%, 68%)`; +} + +function bellDotGlow(assignedBell) { + const bell = Number(assignedBell || 0); + if (bell <= 0) return "rgba(100,116,139,0.35)"; + const t = Math.min(1, Math.max(0, (bell - 1) / 15)); + const hue = interpolateHue(t); + return `hsla(${hue}, 78%, 56%, 0.45)`; } const mutedStyle = { color: "var(--text-muted)" }; @@ -490,11 +517,11 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet height: "68%", borderRadius: "9999px", backgroundColor: isUnassigned ? "rgba(100,116,139,0.7)" : bellDotColor(assignedBell), - color: "var(--text-white)", + color: "#111827", opacity: dotVisible ? 1 : 0, transform: dotVisible ? "scale(1)" : "scale(0.4)", boxShadow: dotVisible - ? (isUnassigned ? "0 0 8px 2px rgba(100,116,139,0.35)" : `0 0 10px 3px ${bellDotColor(assignedBell)}66`) + ? (isUnassigned ? "0 0 8px 2px rgba(100,116,139,0.35)" : `0 0 10px 3px ${bellDotGlow(assignedBell)}`) : "none", transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease", }}