diff --git a/src/activities/piano_composition/MultipleStaff.qml b/src/activities/piano_composition/MultipleStaff.qml index 03b3e5faa..5d56724e2 100644 --- a/src/activities/piano_composition/MultipleStaff.qml +++ b/src/activities/piano_composition/MultipleStaff.qml @@ -1,604 +1,605 @@ /* GCompris - MultipleStaff.qml * * Copyright (C) 2016 Johnny Jazeix * Copyright (C) 2018 Aman Kumar Gupta * * Authors: * Beth Hadley (GTK+ version) * Johnny Jazeix (Qt Quick port) * Aman Kumar Gupta (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import GCompris 1.0 import "../../core" import "qrc:/gcompris/src/activities/piano_composition/NoteNotations.js" as NoteNotations Item { id: multipleStaff property int nbStaves property string clef property int distanceBetweenStaff: multipleStaff.height / 3.3 readonly property real clefImageWidth: 3 * height / 25 // Stores the note index which is selected. property int selectedIndex: -1 // The notes that are to be colored can be assigned to this variable in the activity property var coloredNotes: [] // When the notesColor is inbuilt, the default color mapping will be done, else assign any color externally in the activity. Example: Reference notes in note_names are red colored. property string notesColor: "inbuilt" property bool noteHoverEnabled: false // Stores if the notes are to be centered on the staff. Used in Play_piano and Play_rhythm. property bool centerNotesPosition: false property bool isPulseMarkerDisplayed: false property bool noteAnimationEnabled: false readonly property bool isMusicPlaying: musicTimer.running property alias flickableStaves: flickableStaves property alias musicElementModel: musicElementModel property alias musicElementRepeater: musicElementRepeater property real flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 3.5 readonly property real pulseMarkerX: pulseMarker.x readonly property bool isPulseMarkerRunning: pulseMarkerAnimation.running property bool isFlickable: true property bool enableNotesSound: true property int currentEnteringStaff: 0 property int bpmValue: 60 // The position where the 1st note in the centered state is to be placed. property real firstCenteredNotePosition: multipleStaff.width / 3.3 property real spaceBetweenNotes: 0 /** * Emitted when a note is clicked. * * It is used for selecting note to play, erase and do other operations on it. */ signal noteClicked(int noteIndex) /** * Emitted when the animation of the note from the right of the staff to the left is finished. * * It's used in note_names activity. */ signal noteAnimationFinished /** * Emitted when the pulseMarker's animation is finished. */ signal pulseMarkerAnimationFinished /** * Used in play_rhythm activity. It tells the instants when pulseMarker reaches a note and the drum sound is to be played. */ signal playDrumSound ListModel { id: musicElementModel } Flickable { id: flickableStaves interactive: multipleStaff.isFlickable flickableDirection: Flickable.VerticalFlick contentWidth: staffColumn.width contentHeight: staffColumn.height + distanceBetweenStaff anchors.fill: parent clip: true Behavior on contentY { NumberAnimation { duration: 250 } } Column { id: staffColumn + z: 10 spacing: distanceBetweenStaff anchors.top: parent.top anchors.topMargin: flickableTopMargin Repeater { id: staves model: nbStaves Staff { id: staff height: multipleStaff.height / 5 width: multipleStaff.width - 5 lastPartition: index == (nbStaves - 1) } } } Repeater { id: musicElementRepeater model: musicElementModel MusicElement { id: musicElement noteName: noteName_ noteType: noteType_ highlightWhenPlayed: highlightWhenPlayed_ noteIsColored: multipleStaff.coloredNotes.indexOf(noteName[0]) != -1 soundPitch: soundPitch_ clefType: clefType_ elementType: elementType_ isDefaultClef: isDefaultClef_ property int staffNb: staffNb_ property alias noteAnimation: noteAnimation // The shift which the elements experience when a sharp/flat note is added before them. readonly property real sharpShiftDistance: blackType != "" ? width / 6 : 0 noteDetails: multipleStaff.getNoteDetails(noteName, noteType, clefType) MouseArea { id: noteMouseArea anchors.fill: parent hoverEnabled: true onClicked: multipleStaff.noteClicked(index) } function highlightNote() { highlightTimer.start() } readonly property real defaultXPosition: musicElementRepeater.itemAt(index - 1) ? (musicElementRepeater.itemAt(index - 1).width + musicElementRepeater.itemAt(index - 1).x) : 0 x: { if(multipleStaff.noteAnimationEnabled) return NaN // !musicElementRepeater.itemAt(index - 1) acts as a fallback condition when there is no previous element present. It happens when Qt clears the model internally. if(isDefaultClef || !musicElementRepeater.itemAt(index - 1)) return 0 else if(musicElementRepeater.itemAt(index - 1).elementType === "clef") { if(centerNotesPosition) return sharpShiftDistance + defaultXPosition + multipleStaff.firstCenteredNotePosition else return sharpShiftDistance + defaultXPosition + 10 } else return sharpShiftDistance + defaultXPosition + multipleStaff.spaceBetweenNotes } onYChanged: { if(noteAnimationEnabled && elementType === "note") noteAnimation.start() } y: { if(elementType === "clef") return flickableTopMargin + staves.itemAt(staffNb).y else if(noteDetails === undefined || staves.itemAt(staffNb) == undefined) return 0 var verticalDistanceBetweenLines = staves.itemAt(0).verticalDistanceBetweenLines var shift = -verticalDistanceBetweenLines / 2 var relativePosition = noteDetails.positionOnStaff var imageY = flickableTopMargin + staves.itemAt(staffNb).y + 2 * verticalDistanceBetweenLines if(rotation === 180) { return imageY - (4 - relativePosition) * verticalDistanceBetweenLines + shift } return imageY - (6 - relativePosition) * verticalDistanceBetweenLines + shift } NumberAnimation { id: noteAnimation target: musicElement properties: "x" duration: 9000 from: multipleStaff.width - 10 to: multipleStaff.clefImageWidth onStopped: { noteAnimationFinished() } } } } Image { id: secondStaffDefaultClef sourceSize.width: musicElementModel.count ? multipleStaff.clefImageWidth : 0 y: staves.count === 2 ? flickableTopMargin + staves.itemAt(1).y : 0 visible: (currentEnteringStaff === 0) && (nbStaves === 2) source: background.clefType ? "qrc:/gcompris/src/activities/piano_composition/resource/" + background.clefType.toLowerCase() + "Clef.svg" : "" } } Rectangle { id: pulseMarker width: activity.horizontalLayout ? 5 : 3 border.width: width / 2 height: staves.itemAt(0) == undefined ? 0 : 4 * staves.itemAt(0).verticalDistanceBetweenLines + width opacity: isPulseMarkerDisplayed && pulseMarkerAnimation.running color: "red" y: flickableTopMargin property real nextPosition: 0 NumberAnimation { id: pulseMarkerAnimation target: pulseMarker property: "x" to: pulseMarker.nextPosition onStarted: { if(pulseMarker.height == 0 && staves.count != 0) { pulseMarker.height = Qt.binding(function() {return 4 * staves.itemAt(0).verticalDistanceBetweenLines + pulseMarker.width;}) } } onStopped: { if(pulseMarker.x === multipleStaff.width) pulseMarkerAnimationFinished() else playDrumSound() } } } /** * Initializes the default clefs on the staves. * * @param clefType: The clef type to be initialized. */ function initClefs(clefType) { musicElementModel.clear() musicElementModel.append({ "elementType_": "clef", "clefType_": clefType, "staffNb_": 0, "isDefaultClef_": true, "noteName_": "", "noteType_": "", "soundPitch_": clefType, "highlightWhenPlayed_": false }) } /** * Pauses the sliding animation of the notes. */ function pauseNoteAnimation() { for(var i = 0; i < musicElementModel.count; i++) { if(musicElementRepeater.itemAt(i).noteAnimation.running) musicElementRepeater.itemAt(i).noteAnimation.pause() } } function resumeNoteAnimation() { for(var i = 0; i < musicElementModel.count; i++) { musicElementRepeater.itemAt(i).noteAnimation.resume() } } /** * Gets all the details of any note like note image, position on staff etc. from NoteNotations. */ function getNoteDetails(noteName, noteType, clefType) { var notesDetails = NoteNotations.get() var noteNotation if(noteType === "Rest") noteNotation = noteName + noteType else noteNotation = clefType + noteName for(var i = 0; i < notesDetails.length; i++) { if(noteNotation === notesDetails[i].noteName) { return notesDetails[i] } } } /** * Adds a note to the staff. */ function addMusicElement(elementType, noteName, noteType, highlightWhenPlayed, playAudio, clefType, soundPitch, isUnflicked) { if(soundPitch == undefined || soundPitch === "") soundPitch = clefType var isNextStaff = (selectedIndex == -1) && musicElementModel.count && ((staves.itemAt(0).width - musicElementRepeater.itemAt(musicElementModel.count - 1).x - musicElementRepeater.itemAt(musicElementModel.count - 1).width) < multipleStaff.clefImageWidth) // If the incoming element is a clef, make sure that there is enough required space to fit one more note too. Else it creates problem when the note is erased and the view is redrawn, else move on to the next staff. if(elementType === "clef" && musicElementModel.count && (selectedIndex == -1)) { if(staves.itemAt(0).width - musicElementRepeater.itemAt(musicElementModel.count - 1).x - musicElementRepeater.itemAt(musicElementModel.count - 1).width - 2 * Math.max(multipleStaff.clefImageWidth, musicElementRepeater.itemAt(0).noteImageWidth) < 0) isNextStaff = true } if(isNextStaff && !noteAnimationEnabled) { multipleStaff.currentEnteringStaff++ if(multipleStaff.currentEnteringStaff >= multipleStaff.nbStaves) multipleStaff.nbStaves++ // When a new staff is added, initialise it with a default clef. musicElementModel.append({"noteName_": "", "noteType_": "", "soundPitch_": soundPitch, "clefType_": clefType, "highlightWhenPlayed_": false, "staffNb_": multipleStaff.currentEnteringStaff, "isDefaultClef_": true, "elementType_": "clef"}) if(!isUnflicked) flickableStaves.flick(0, - nbStaves * multipleStaff.height) if(elementType === "clef") return 0 isNextStaff = false } if(selectedIndex === -1) { var isDefaultClef = false if(!musicElementModel.count) isDefaultClef = true musicElementModel.append({"noteName_": noteName, "noteType_": noteType, "soundPitch_": soundPitch, "clefType_": clefType, "highlightWhenPlayed_": highlightWhenPlayed, "staffNb_": multipleStaff.currentEnteringStaff, "isDefaultClef_": isDefaultClef, "elementType_": elementType}) } else { var tempModel = createNotesBackup() var insertingIndex = selectedIndex + 1 if(elementType === "clef") insertingIndex-- tempModel.splice(insertingIndex, 0, {"elementType_": elementType, "noteName_": noteName, "noteType_": noteType, "soundPitch_": soundPitch, "clefType_": clefType }) if(elementType === "clef") { for(var i = 0; i < musicElementModel.count && tempModel[i]["elementType_"] != "clef"; i++) tempModel[i]["soundPitch_"] = clefType } selectedIndex = -1 redraw(tempModel) } multipleStaff.selectedIndex = -1 background.clefType = musicElementModel.get(musicElementModel.count - 1).soundPitch_ if(playAudio) playNoteAudio(noteName, noteType, soundPitch, musicElementRepeater.itemAt(musicElementModel.count - 1).duration) } /** * Creates a backup of the musicElementModel before erasing it. * * This backup data is used to redraw the notes. */ function createNotesBackup() { var tempModel = [] for(var i = 0; i < musicElementModel.count; i++) tempModel.push(JSON.parse(JSON.stringify(musicElementModel.get(i)))) return tempModel } /** * Redraws all the notes on the staves. */ function redraw(notes) { musicElementModel.clear() currentEnteringStaff = 0 selectedIndex = -1 for(var i = 0; i < notes.length; i++) { var note = notes[i] // On load melody from file, the first "note" is the BPM value if(note.bpm) { bpmValue = note.bpm; } else { addMusicElement(note["elementType_"], note["noteName_"], note["noteType_"], false, false, note["clefType_"], note["soundPitch_"], true) } } // Remove the remaining unused staffs. if((multipleStaff.currentEnteringStaff + 1 < multipleStaff.nbStaves) && (multipleStaff.nbStaves > 2)) { nbStaves = multipleStaff.currentEnteringStaff + 1 flickableStaves.flick(0, - nbStaves * multipleStaff.height) } var lastMusicElement = musicElementModel.get(musicElementModel.count - 1) if(lastMusicElement.isDefaultClef_ && nbStaves > 2) { musicElementModel.remove(musicElementModel.count - 1) lastMusicElement = musicElementModel.get(musicElementModel.count - 1) } if(lastMusicElement.staffNb_ < nbStaves - 1 && nbStaves != 2) nbStaves = lastMusicElement.staffNb_ + 1 currentEnteringStaff = lastMusicElement.staffNb_ background.clefType = lastMusicElement.soundPitch_ } /** * Erases the selected note. * * @param noteIndex: index of the note to be erased */ function eraseNote(noteIndex) { musicElementModel.remove(noteIndex) selectedIndex = -1 var tempModel = createNotesBackup() redraw(tempModel) } /** * Erases all the notes. */ function eraseAllNotes() { musicElementModel.clear() selectedIndex = -1 multipleStaff.currentEnteringStaff = 0 initClefs(background.clefType) } readonly property var octave1MidiNumbersTable: {"C":24,"C#":25,"Db":25,"D":26,"D#":27,"Eb":27,"E":28,"F":29,"F#":30,"Gb":30,"G":31,"G#":32,"Ab":32,"A":33,"A#":34,"Bb":34,"B":35} /** * Plays audio for a note. * * @param noteName: name of the note to be played. * @param noteType: note type to be played. */ function playNoteAudio(noteName, noteType, soundPitch, duration) { if(noteName) { if(noteType != "Rest") { // We should find a corresponding b type enharmonic notation for # type note to play the audio. if(noteName[1] === "#") { var blackKeysFlat = piano.blackKeyFlatNoteLabelsArray var blackKeysSharp = piano.blackKeySharpNoteLabelsArray for(var i = 0; i < blackKeysSharp.length; i++) { if(blackKeysSharp[i][0] === noteName) { noteName = blackKeysFlat[i][0] break } } } var octaveNb = "" var noteCharName = "" if(noteName[1] == "#" || noteName[1] == "b") { noteCharName = noteName[0] + noteName[1] octaveNb = noteName[2] } else { noteCharName = noteName[0] octaveNb = noteName[1] } var noteMidiName = (octaveNb-1)*12 + octave1MidiNumbersTable[noteCharName]; GSynth.generate(noteMidiName, duration) } } } /** * Get all the notes from the musicElementModel and returns the melody. */ function getAllNotes() { var notes = createNotesBackup() return notes } /** * Loads melody from the provided data, to the staffs. * * @param data: melody to be loaded */ function loadFromData(data) { if(data != undefined) { var melody = data.split(" ") background.clefType = melody[0] eraseAllNotes() for(var i = 1 ; i < melody.length; ++i) { var noteLength = melody[i].length var noteName = melody[i][0] var noteType if(melody[i].substring(noteLength - 4, noteLength) === "Rest") { noteName = melody[i].substring(0, noteLength - 4) noteType = "Rest" } else if(melody[i][1] === "#" || melody[i][1] === "b") { noteType = melody[i].substring(3, melody[i].length) noteName += melody[i][1] + melody[i][2]; } else { noteType = melody[i].substring(2, melody[i].length) noteName += melody[i][1] } addMusicElement("note", noteName, noteType, false, false, melody[0]) } var tempModel = createNotesBackup() redraw(tempModel) } } /** * Used in the activity play_piano. * * Checks if the answered note is correct */ function indicateAnsweredNote(isCorrectAnswer, noteIndexAnswered) { musicElementRepeater.itemAt(noteIndexAnswered).noteAnswered = true musicElementRepeater.itemAt(noteIndexAnswered).isCorrectlyAnswered = isCorrectAnswer } /** * Used in the activity play_piano. * * Reverts the previous answer. */ function revertAnswer(noteIndexReverting) { musicElementRepeater.itemAt(noteIndexReverting).noteAnswered = false } function play() { musicTimer.currentNote = 0 selectedIndex = -1 musicTimer.interval = 1 if(isFlickable) flickableStaves.flick(0, nbStaves * multipleStaff.height) pulseMarkerAnimation.stop() if(musicElementModel.count > 1) pulseMarker.x = musicElementRepeater.itemAt(1).x + musicElementRepeater.itemAt(1).width / 2 else pulseMarker.x = 0 musicTimer.start() } /** * Stops the audios playing. */ function stopAudios() { musicElementModel.clear() musicTimer.stop() items.audioEffects.stop() } Timer { id: musicTimer property int currentNote: 0 onRunningChanged: { if(!running && musicElementModel.get(currentNote) !== undefined) { var currentElement = musicElementModel.get(currentNote) var currentType = currentElement.noteType_ var note = currentElement.noteName_ var soundPitch = currentElement.soundPitch_ var currentStaff = currentElement.staffNb_ background.clefType = currentElement.clefType_ if(currentElement.isDefaultClef_ && currentStaff > 1) { flickableStaves.contentY = staves.itemAt(currentStaff - 1).y } musicTimer.interval = musicElementRepeater.itemAt(currentNote).duration if(multipleStaff.enableNotesSound) playNoteAudio(note, currentType, soundPitch, musicTimer.interval) pulseMarkerAnimation.stop() pulseMarkerAnimation.duration = Math.max(1, musicTimer.interval) if(musicElementRepeater.itemAt(currentNote + 1) != undefined) pulseMarker.nextPosition = musicElementRepeater.itemAt(currentNote + 1).x + musicElementRepeater.itemAt(currentNote + 1).width / 2 else pulseMarker.nextPosition = multipleStaff.width pulseMarkerAnimation.start() if(!isPulseMarkerDisplayed) musicElementRepeater.itemAt(currentNote).highlightNote() currentNote++ musicTimer.start() } } } } diff --git a/src/activities/piano_composition/MusicElement.qml b/src/activities/piano_composition/MusicElement.qml index 5a6ab26c2..1cfe6d1c3 100644 --- a/src/activities/piano_composition/MusicElement.qml +++ b/src/activities/piano_composition/MusicElement.qml @@ -1,204 +1,203 @@ /* GCompris - musicElement.qml * * Copyright (C) 2016 Johnny Jazeix * Copyright (C) 2018 Aman Kumar Gupta * * Authors: * Beth Hadley (GTK+ version) * Johnny Jazeix (Qt Quick port) * Aman Kumar Gupta (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import QtGraphicalEffects 1.0 import GCompris 1.0 import "../../core" Item { id: musicElement width: noteImageWidth height: multipleStaff.height / 5 property string noteName property string noteType property string soundPitch property string clefType property string elementType property bool noteIsColored: true property bool isDefaultClef: false property string blackType: noteName === "" ? "" : noteName[1] === "#" ? "sharp" : noteName[1] === "b" ? "flat" : ""// empty, "flat" or "sharp" /** * Calculates and assign the timer interval for a note. */ function calculateTimerDuration(noteType) { noteType = noteType.toLowerCase() if(noteType === "whole") return 240000 / multipleStaff.bpmValue else if(noteType === "half") return 120000 / multipleStaff.bpmValue else if(noteType === "quarter") return 60000 / multipleStaff.bpmValue else return 30000 / multipleStaff.bpmValue } readonly property int duration: { if(elementType != "clef") { if(noteType === "Rest") return calculateTimerDuration(noteName) else return calculateTimerDuration(noteType) } return 0 } readonly property real noteImageWidth: (multipleStaff.width - 15 - clefImageWidth) / 10 readonly property var noteColorMap: { "1": "#FF0000", "2": "#FF7F00", "3": "#FFFF00", "4": "#32CD32", "5": "#6495ED", "6": "#D02090", "7": "#FF1493", "8": "#FF0000", "-1": "#FF6347", "-2": "#FFD700", "-3": "#20B2AA", "-4": "#8A2BE2", "-5": "#FF00FF" } readonly property var whiteNoteName: { "C": "1", "D": "2", "E": "3", "F": "4", "G": "5", "A": "6", "B": "7", "C": "8" } readonly property var sharpNoteName: { "C#": "-1", "D#": "-2", "F#": "-3", "G#": "-4", "A#": "-5" } readonly property var flatNoteName: { "Db": "-1", "Eb": "-2", "Gb": "-3", "Ab": "-4", "Bb": "-5" } readonly property var blackNoteName: blackType == "" ? blackType : blackType == "flat" ? flatNoteName : sharpNoteName property bool highlightWhenPlayed: false property alias highlightTimer: highlightTimer property var noteDetails property bool noteAnswered: false property bool isCorrectlyAnswered: false rotation: { if((noteDetails === undefined) || elementType === "clef") return 0 else if((noteDetails.positionOnStaff < 0) && (noteType === "Whole")) return 180 else return noteDetails.rotation } Image { id: blackTypeImage source: blackType !== "" ? "qrc:/gcompris/src/activities/piano_composition/resource/black" + blackType + ".svg" : "" sourceSize.width: noteImage.width / 2 anchors.right: parent.rotation === 180 ? undefined : noteImage.left anchors.left: parent.rotation === 180 ? noteImage.right : undefined rotation: parent.rotation === 180 ? 180 : 0 anchors.rightMargin: -noteImage.width / 4 anchors.leftMargin: -noteImage.width / 2.5 anchors.bottom: noteImage.bottom anchors.bottomMargin: parent.height / 6 fillMode: Image.PreserveAspectFit } Rectangle { id: highlightRectangle width: musicElement.width height: musicElement.height * 0.9 color: "transparent" opacity: 1 border.color: "#373737" border.width: radius * 0.5 radius: width * 0.1 visible: (multipleStaff.noteHoverEnabled && noteMouseArea.containsMouse) || highlightTimer.running } Rectangle { id: selectedNoteIndicator width: musicElement.width height: musicElement.height * 0.9 color: "blue" opacity: 0.6 border.color: "white" radius: width / 5 visible: selectedIndex == index } Image { id: noteImage source: (noteDetails === undefined) ? "" : noteType != "Rest" ? "qrc:/gcompris/src/activities/piano_composition/resource/" + noteDetails.imageName + noteType + ".svg" : "qrc:/gcompris/src/activities/piano_composition/resource/" + noteDetails.imageName + ".svg" sourceSize.width: 200 width: musicElement.width height: musicElement.height mirror: parent.rotation == 180 && parent.noteType == "Eighth" ? true : false } Image { id: clefImage source: (elementType === "clef") ? "qrc:/gcompris/src/activities/piano_composition/resource/" + clefType.toLowerCase() + "Clef.svg" : "" sourceSize.width: multipleStaff.clefImageWidth } Image { id: correctOrWrongAnswerIndicator visible: noteAnswered source: isCorrectlyAnswered ? "qrc:/gcompris/src/activities/piano_composition/resource/passed.svg" : "qrc:/gcompris/src/activities/piano_composition/resource/failed.svg" sourceSize.width: noteImage.width / 2.5 anchors.right: parent.rotation === 180 ? undefined : noteImage.right anchors.left: parent.rotation === 180 ? noteImage.left : undefined rotation: parent.rotation === 180 ? 180 : 0 anchors.rightMargin: 12 anchors.bottom: noteImage.bottom anchors.bottomMargin: parent.height / 6 fillMode: Image.PreserveAspectFit z: 3 } Rectangle { id:softColor readonly property int invalidConditionNumber: -6 readonly property int noteColorNumber: { if(noteDetails === undefined || noteType === "" || noteType === "Rest" || noteName === "") return invalidConditionNumber else if((blackType === "") && (whiteNoteName[noteName[0]] != undefined)) return whiteNoteName[noteName[0]] else if((noteName.length > 2) && (blackNoteName[noteName.substring(0,2)] != undefined)) return blackNoteName[noteName.substring(0,2)] else return invalidConditionNumber } color: { if(multipleStaff.notesColor === "inbuilt") - return (noteColorNumber > invalidConditionNumber) ? noteColorMap[noteColorNumber] : "white" // make image like it lays under red glass + return (noteColorNumber > invalidConditionNumber) ? noteColorMap[noteColorNumber] : "white" else return multipleStaff.notesColor } - z: -10 + z: -1 width: noteImage.width * 0.8 height: width radius: width * 0.5 anchors.centerIn: noteImage - opacity: 0.8 visible: noteIsColored && (elementType != "clef") } Timer { id: highlightTimer interval: duration } }