Initial commit: InTono — app KMP per l'apprendimento musicale
App Kotlin Multiplatform (Android/iOS/Desktop) con Compose Multiplatform: controllo intonazione (YIN), pratica guidata, metronomo, ear training, sfide, scansione spartiti via AI, spartiti bundled e pentagramma con glifi tipografici, travature, battute e spaziatura proporzionale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
21ad969b10
245 files changed
+28963
No files matched your search
@@ -0,0 +1,170 @@
|
||||
package com.notemusicali.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.notemusicali.music.MusicalNote
|
||||
|
||||
/**
|
||||
* Shows a window of notes around the current index, with the current note highlighted.
|
||||
* [windowSize] controls how many notes are visible (odd number works best).
|
||||
*/
|
||||
@Composable
|
||||
fun StaffContextView(
|
||||
notes: List<MusicalNote>,
|
||||
currentIndex: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
windowSize: Int = 7,
|
||||
lightTheme: Boolean = false,
|
||||
beats: Int = 4,
|
||||
beatType: Int = 4,
|
||||
) {
|
||||
if (notes.isEmpty()) return
|
||||
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp),
|
||||
) {
|
||||
val lineColor = if (lightTheme) Color.Black else Color.White.copy(alpha = 0.4f)
|
||||
val dimNoteColor = if (lightTheme) Color.Black else Color.White.copy(alpha = 0.35f)
|
||||
val playedNoteColor = if (lightTheme) Color.Black.copy(alpha = 0.35f) else Color.White.copy(alpha = 0.2f)
|
||||
val currentNoteColor = Color(0xFFFFD54F)
|
||||
val clefColor = if (lightTheme) Color.Black else Color.White.copy(alpha = 0.8f)
|
||||
|
||||
val lineSpacing = size.height / 10f
|
||||
val staffBottom = size.height / 2f + lineSpacing * 2f
|
||||
val halfSpacing = lineSpacing / 2f
|
||||
val leftMargin = size.width * 0.06f
|
||||
val rightMargin = size.width * 0.02f
|
||||
|
||||
val useTreble = StaffLayout.useTrebleClef(notes)
|
||||
|
||||
// Staff lines
|
||||
for (i in 0..4) {
|
||||
val y = staffBottom - i * lineSpacing
|
||||
drawLine(lineColor, Offset(leftMargin, y), Offset(size.width - rightMargin, y), 1.5f)
|
||||
}
|
||||
|
||||
// Determine visible window
|
||||
val half = windowSize / 2
|
||||
val startIdx = (currentIndex - half).coerceAtLeast(0)
|
||||
val endIdx = (startIdx + windowSize).coerceAtMost(notes.size)
|
||||
val visibleNotes = notes.subList(startIdx, endIdx)
|
||||
|
||||
// Clef (+ time signature when the window includes the start of the piece)
|
||||
var cursorX = leftMargin + 2f
|
||||
cursorX += drawClef(textMeasurer, useTreble, cursorX, staffBottom, lineSpacing, clefColor)
|
||||
if (startIdx == 0) {
|
||||
cursorX += 6f
|
||||
cursorX += drawTimeSignature(textMeasurer, beats, beatType, cursorX, staffBottom, lineSpacing, clefColor)
|
||||
}
|
||||
|
||||
val noteAreaStart = cursorX + 10f
|
||||
val noteAreaEnd = size.width - rightMargin - 8f
|
||||
val noteAreaWidth = noteAreaEnd - noteAreaStart
|
||||
|
||||
val xs = StaffLayout.xPositions(visibleNotes, noteAreaStart, noteAreaWidth)
|
||||
// Beam groups on the full sequence, clipped to the visible window (window-local indices)
|
||||
val groups = StaffLayout.beamGroups(notes, beats, beatType)
|
||||
.mapNotNull { g ->
|
||||
val from = maxOf(g.first, startIdx)
|
||||
val to = minOf(g.last, endIdx - 1)
|
||||
if (to - from >= 1) (from - startIdx)..(to - startIdx) else null
|
||||
}
|
||||
val grouped = groups.flatMap { it }.toSet()
|
||||
val bars = StaffLayout.measurePositions(notes, beats, beatType).toSet()
|
||||
|
||||
val headHalf = staffHeadHalfWidth(lineSpacing)
|
||||
val ledgerHalf = lineSpacing * 0.8f
|
||||
|
||||
visibleNotes.forEachIndexed { vi, note ->
|
||||
val actualIdx = startIdx + vi
|
||||
val nx = xs[vi]
|
||||
|
||||
val isCurrent = actualIdx == currentIndex
|
||||
val isPlayed = actualIdx < currentIndex
|
||||
val color = when {
|
||||
isCurrent -> currentNoteColor
|
||||
isPlayed -> playedNoteColor
|
||||
else -> dimNoteColor
|
||||
}
|
||||
|
||||
val relPos = StaffLayout.relativePosition(note, useTreble)
|
||||
val ny = staffBottom - relPos * halfSpacing
|
||||
|
||||
// Ledger lines
|
||||
var lp = -2
|
||||
while (lp >= relPos) {
|
||||
val ly = staffBottom - lp * halfSpacing
|
||||
drawLine(lineColor.copy(alpha = 0.3f), Offset(nx - ledgerHalf, ly), Offset(nx + ledgerHalf, ly), 1f)
|
||||
lp -= 2
|
||||
}
|
||||
lp = 10
|
||||
while (lp <= relPos) {
|
||||
val ly = staffBottom - lp * halfSpacing
|
||||
drawLine(lineColor.copy(alpha = 0.3f), Offset(nx - ledgerHalf, ly), Offset(nx + ledgerHalf, ly), 1f)
|
||||
lp += 2
|
||||
}
|
||||
|
||||
// Note head
|
||||
drawNoteHeadPath(nx, ny, lineSpacing, note.duration, color)
|
||||
|
||||
// Stem + flags (only for notes not part of a beamed group)
|
||||
if (vi !in grouped) {
|
||||
val stemUp = relPos < 4
|
||||
val stemTip = drawStem(nx, ny, lineSpacing, stemUp, note.duration, color)
|
||||
stemTip?.let { drawCurvedFlags(it, stemUp, note.duration, lineSpacing, color) }
|
||||
}
|
||||
|
||||
// Sharp
|
||||
drawAccidentalGlyph(note, nx - headHalf - lineSpacing * 0.5f, ny, lineSpacing * 0.35f, color)
|
||||
|
||||
// Barline between this note and the next
|
||||
if (actualIdx in bars && vi < visibleNotes.lastIndex) {
|
||||
drawBarline((xs[vi] + xs[vi + 1]) / 2f, staffBottom, lineSpacing, lineColor)
|
||||
}
|
||||
|
||||
// Current note highlight circle
|
||||
if (isCurrent) {
|
||||
drawCircle(currentNoteColor.copy(alpha = 0.15f), radius = lineSpacing * 2f, center = Offset(nx, ny))
|
||||
}
|
||||
|
||||
// Note name under current
|
||||
if (isCurrent) {
|
||||
val labelStyle = TextStyle(color = currentNoteColor, fontSize = 12.sp)
|
||||
val labelResult = textMeasurer.measure(note.fullName, labelStyle)
|
||||
drawText(labelResult, topLeft = Offset(nx - labelResult.size.width / 2f, staffBottom + lineSpacing * 1.2f))
|
||||
}
|
||||
}
|
||||
|
||||
// Beamed groups
|
||||
val currentVi = currentIndex - startIdx
|
||||
groups.forEach { group ->
|
||||
val gxs = group.map { xs[it] }
|
||||
val relPositions = group.map { StaffLayout.relativePosition(visibleNotes[it], useTreble) }
|
||||
val gys = relPositions.map { staffBottom - it * halfSpacing }
|
||||
val gdurations = group.map { visibleNotes[it].duration }
|
||||
val groupColor = if (currentVi in group) currentNoteColor else dimNoteColor
|
||||
drawBeamedGroup(
|
||||
xs = gxs,
|
||||
ys = gys,
|
||||
durations = gdurations,
|
||||
stemUp = StaffLayout.stemUpForGroup(relPositions),
|
||||
lineSpacing = lineSpacing,
|
||||
color = groupColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user