Search code examples
javascriptcanvasonclickweb-audio-api

click functions through references variables on a canvas-possible?


Just run into a bit of wall with my current project. So for my comp musics course we have to create a 24 key (2 octave) keyboard by first rendering a keyboard using a canvas and then using web audio to loading and play 24 different sounds. I have gotten my clips successfully loaded into an array(or so I hope!) but am a bit confused as to how I would go about handling click events and playing each sound. Searching around the internet has only yielded results about handling click events through finding the coordinates of the click and performing a certain event through that. That may not work for my project as I first render the white keys (14 of them) and then rendering the black keys on top (10 of those). That would make it difficult to detect clicks via coordinates as the white keys are no longer rectangle. My code for rendering the keys looks as such

function createKeys() {
var r = document.getElementById('piano');
var key = r.getContext('2d');
// creates white piano keys
key.beginPath();
for (i = 0; i < 14; i++)
{
wKey = [key.rect(i*70, 0, 70, 120)];
key.fillStyle = "#FFFFFF";
key.fill();
key.lineWidth = 2;
key.strokeStyle = 'black';
key.stroke();

}
key.closePath();

// begin black keys 
key.beginPath();
var bKey1 = key.rect(53, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey2 = key.rect(123, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey3 = key.rect(263, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey4 = key.rect(333, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey5 = key.rect(403, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey6 = key.rect(543, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey7 = key.rect(613, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey8 = key.rect(753, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey9 = key.rect(823, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

var bKey10 = key.rect(893, 0, 35, 80);
key.fillStyle = "#000000";
key.fill();
key.lineWidth= 2;
key.strokeStyle = '#888888';
key.stroke();

key.closePath();
}

Now as you can see I created a reference variable for the white keys, an array called 'wKey' with indices 0 through 13(to represent each key), and individual variables for black keys called bKey1-10, because figuring out a formula to such a pattern was frying my brain. I was just wondering if I could create a function to check if those references were clicked instead of using coordinate tracking to perform actions (such as changing the color and playing the soundfile) on each key. Ideally I would like to do something along the lines of

 If wKey[i] = clicked
 then
 play soundfile[i]
 else if bKey1 = clicked
 play soundfile[14]
 else if bKey2 = clicked
 play soundfile[15]
 ... and so on

Not sure of the feasibility of this as I've never had to mess around with a canvas, let alone performing functions on a canvas. Would like to hear what some fresh minds have to say about this.

EDIT: Since I'm creating a canvas I figured posting the HTML might be beneficial

<body>
    <h1><u>The Cory Matthews "UNDAPANTS" Piano</u> by Chris C.</h1>
    <div id = "controls_toolbar">



    </div>
    <canvas id="piano" width = "980" height = "120" style ="border:1px solid #000000;" class="center" onclick ="keyClicked()"> </canvas>
        <script>
            const PATH = '/mp3/'
                  SOUNDS = ['DOWNUnderPantsC', 'DOWNUnderPantsD', 'DOWNUnderPantsE', 'DOWNUnderPantsF', 'DOWNUnderPantsG',
                  'DOWNUnderPantsA', 'DOWNUnderPantsB', 'UPUnderPantsC', 'UPUnderPantsD', 'UPUnderPantsE', 'UPUnderPantsF',
              'UPUnderPantsG', 'UPUnderPantsA', 'UPUnderPantsB', 'DOWNUnderPantsC#', 'DOWNUnderPantsD#', 'DOWNUnderPantsF#',
          'DOWNUnderPantsG#', 'DOWNUnderPantsA#', 'UPUnderPantsC#', 'UPUnderPantsD#', 'UPUnderPantsF#', 'UPUnderPantsG#', 'UPUnderPantsA#']
            createKeys();
            init();
            fetchSounds();
        </script>
    <p>Volume: <input type="range" min="0" max="100" value="100" oninput="VolumeSample.changeVolume(this);" /></p>

    <img class="center" src="images/cm.png" alt="UNDAPANTS" style="margin-top: 20px">
</body>

Solution

  • Basically you have to implement a manual button solution for canvas. This is not so complicated as it sounds as (a map is btw. also a way but I'm not covering it here).

    This solution is dynamic which means you can expand the number of octaves, size of canvas etc. all everything will adjust automatically.

    First let the notes array defined the number of keys in each octave as well as which keys should be black; we'll use the # appendix to determine that:

    var notes = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
    var keys = notes.length * octaves;
    

    Please refer to the source below on how the render is performed in detail.

    Looping through the number of keys using a separate index for white keys (as the black keys are relative to those) we get a full piano keyboard. We store the calculated x positions of the keys in two different arrays, one for black keys and one for whites - this makes it easier to do hit tests later (alternative is to store key shapes - max 4 - and use those as hit test paths).

    When the piano is rendered we can check for mouse down events (if you want to be able to drag when mouse is down to play different keys you need to set a down flag instead when mouse is down and then use the rest of the code inside a mouse move events with some optimizations - not shown here).

    // check for mouse down
    canvas.addEventListener('mousedown', function(e) {
    
        // adjust mouse position
        var rect = canvas.getBoundingClientRect(),
            x = e.clientX - rect.left,
            y = e.clientY - rect.top,
            i, a;
    
        // fill color for key down
        ctx.fillStyle = '#fa2';
    
        //in blacks?
        for(i = 0; a = arrayBlacks[i++];) {
            ctx.beginPath();                               // start new path for test
            ctx.rect(a[0], 0, blackKeyWidth - 2, h * .67); // add a rect to path
            if (ctx.isPointInPath(x, y)) {                 // test if point is in path
                ctx.fill();                                // yes, fill it
                outputKey(a[1], a[2]);                     // show/play note
                return;
            }
        }
    
        //in whites? (same as above, but for arrayWhites)
        for(i = 0; a = arrayWhites[i++];) {
        ...cut... see full source
    
    }, false);
    

    When mouse is released we just re-render everything for simplicity. For larger keyboards (additional octaves) you might want to consider a per-path object approach where each key is stored as shape.

    // if mouse up, re-render all
    canvas.addEventListener('mouseup', function(e) {
        renderPiano(false);
    }, false);
    

    All details can be found in the attached live snippet. Hope this helps!

    // some initial values/setup
    var canvas = document.getElementById('piano'),  // get canvas
        ctx = canvas.getContext('2d'),              // get context
        h = canvas.height,                          // cache dimension
        w = canvas.width,
        
        octaves = 2,                                // octaves to render
        notes = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'],
        keys = notes.length * octaves,              // pre-calc num of keys
        noteIndex = 0,                              // note index
        keyIndex = 0,                               // white key index
        note,                                       // note
        whiteKeyWidth = (w / (7 * octaves))|0,      // width of whites
        blackKeyWidth = whiteKeyWidth * 0.75,       // width of blacks
        i,                                          // iterator
        x,                                          // x pos of key
        
        arrayBlacks = [],                           // store black positions
        arrayWhites = []                            // store white positions
    ;
    
    // calc piano key positions
    
    for (i = 0; i < keys; i++) {
    
        noteIndex = i % notes.length;               // force within notes range
        note = notes[noteIndex];                    // get note name
        x = keyIndex * whiteKeyWidth;               // start pos. of key
        
        if (note.length === 1) {                    // white key (no sharp)
            arrayWhites.push([x, note, i]);         // store position
            keyIndex++;                             // next white key index
        }
        else {
            x -= blackKeyWidth * .5;                // adjust for black key
            arrayBlacks.push([x, note, i]);
        }
    }
    
    renderPiano(false);
    
    // common render function based on whites/blacks arrays
    // special switch: onlyBlacks to override white key rendered while down
    function renderPiano(onlyBlacks) {
    
        var i, a;
        
        //render white keys
        if (!onlyBlacks) {
            ctx.clearRect(0, 0, w, h); // clear canvas for full render
            ctx.fillStyle = '#ffe';
            for(i = 0; a = arrayWhites[i++];) {
                ctx.fillRect(a[0], 0, whiteKeyWidth - 2, h - 1);
                ctx.strokeRect(a[0], 0, whiteKeyWidth - 2, h - 1);
            }
        }
      
        //render black keys
        ctx.fillStyle = '#000';
        for(i = 0; a = arrayBlacks[i++];) {
            ctx.fillRect(a[0], 0, blackKeyWidth - 2, h * .67);
        }
    }
    
    // check for mouse down
    canvas.addEventListener('mousedown', function(e) {
    
        // adjust mouse position
        var rect = canvas.getBoundingClientRect(),
            x = e.clientX - rect.left,
            y = e.clientY - rect.top,
            i, a;
    
        ctx.fillStyle = '#fa2';
    
        //in blacks?
        for(i = 0; a = arrayBlacks[i++];) {
            ctx.beginPath();
            ctx.rect(a[0], 0, blackKeyWidth - 2, h * .67);
            if (ctx.isPointInPath(x, y)) {
                ctx.fill();
                outputKey(a[1], a[2]);
                return;
            }
        }
    
        //in whites?
        for(i = 0; a = arrayWhites[i++];) {
            ctx.beginPath();
            ctx.rect(a[0], 0, whiteKeyWidth - 2, h - 1);
            if (ctx.isPointInPath(x, y)) {
                ctx.fill();
                renderPiano(true);     // render black keys on top!
                outputKey(a[1], a[2]);
                return;
            }
        }
    
    }, false);
    
    // if mouse up, re-render all
    canvas.addEventListener('mouseup', function(e) {
        renderPiano(false);
    }, false);
    
    // format output here, ie. play correct note etc.
    function outputKey(key, index) {
        out.innerHTML = key + ((index / 12)|0);
        // play note
    }
    <canvas id="piano" width=540 height=160></canvas>
    <br><output id="out"></output>