diff --git a/app.js b/app.js index cdb1850..5e91539 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,24 @@ +const MAX_LEVEL = 3 +const DRAW_PERIOD = 0.04 // s +const UPDATE_PERIOD = 0.01 // s +const FLOOR = 400 // px +const FIRST_NOTE = 48 // C2 +const LAST_NOTE = 73 // C4 +const FREQUENCIES = [ + // C C♯ / D♭ D D♯ / E♭ E F F♯ / G♭ G G♯ / A♭ A A♯ / B♭ B + 16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87, + 32.7, 34.65, 36.71, 38.89, 41.2, 43.65, 46.25, 49, 51.91, 55, 58.27, 61.74, + 65.41, 69.3, 73.42, 77.78, 82.41, 87.31, 92.5, 98, 103.83, 110, 116.54, 123.47, + 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185, 196, 207.65, 220, 233.08, 246.94, + 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392, 415.3, 440, 466.16, 493.88, + 523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, 783.99, 830.61, 880, 932.33, 987.77, + 1046.5, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760, 1864.66, 1975.53, + 2093, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520, 3729.31, 3951.07, + 4186.01, 4434.92, 4698.64, 4978.03, 5274.04, 5587.65, 5919.91, 6271.93, 6644.88, 7040, 7458.62, 7902.13, +] +const NOTE_NAMES = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"] + + Array.prototype.remove = function(item) { let index = this.indexOf(item) if (index == 0) { @@ -89,16 +110,18 @@ class Sprite { } -class Canon extends Sprite { +class Cannon extends Sprite { constructor(canvasCtx, note) { let sharp = [1, 3, 6, 8, 10].includes(note % 12) - super(canvasCtx, "canon.png", 34 * (note - FIRST_NOTE) + 66, sharp? 446:450, 11, 26, 4) + super(canvasCtx, "cannon.png", 34 * (note - FIRST_NOTE) + 66, sharp? 422:426, 11, 26, 4) this.note = note + this.key = keyMap[note - FIRST_NOTE].toUpperCase() this.impactHeight = 9 this.impactY = 0 this.sy = sharp? 0 : this.sHeight this.shooting = false this.df = 1 + this.pipeSprite = new Sprite(canvasCtx, "pipe.png", this.x-1, this.y+36, 16, 18) } draw() { @@ -109,6 +132,11 @@ class Canon extends Sprite { } else { if (this.frame > 0) this.frame-- } + this.pipeSprite.draw() + this.canvasCtx.fillStyle = "#d3d6cf" + this.canvasCtx.fillText(this.key, this.pipeSprite.x+2, this.pipeSprite.y+10) + this.canvasCtx.fillStyle = "#222327" + this.canvasCtx.fillText(this.key, this.pipeSprite.x, this.pipeSprite.y+8) super.draw() if (this.frame) { this.canvasCtx.drawImage(this.sprite, this.sWidth*(this.frame), 0, this.sWidth, 1, this.dx, this.impactY, 22, this.dy - this.impactY) @@ -202,23 +230,6 @@ class Whole extends Note { } -const MAX_LEVEL = 2 -const UPDATE_PERIOD = 10 //ms -const FIRST_NOTE = 48 -const LAST_NOTE = 73 -const FREQUENCIES = [ - // C C♯ / D♭ D D♯ / E♭ E F F♯ / G♭ G G♯ / A♭ A A♯ / B♭ B - 16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87, - 32.7, 34.65, 36.71, 38.89, 41.2, 43.65, 46.25, 49, 51.91, 55, 58.27, 61.74, - 65.41, 69.3, 73.42, 77.78, 82.41, 87.31, 92.5, 98, 103.83, 110, 116.54, 123.47, - 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185, 196, 207.65, 220, 233.08, 246.94, - 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392, 415.3, 440, 466.16, 493.88, - 523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, 783.99, 830.61, 880, 932.33, 987.77, - 1046.5, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760, 1864.66, 1975.53, - 2093, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520, 3729.31, 3951.07, - 4186.01, 4434.92, 4698.64, 4978.03, 5274.04, 5587.65, 5919.91, 6271.93, 6644.88, 7040, 7458.62, 7902.13, -] - let keyMap = keyMapInput.value let playing = false @@ -228,27 +239,19 @@ canvasCtx.webkitImageSmoothingEnabled = false canvasCtx.msImageSmoothingEnabled = false canvasCtx.imageSmoothingEnabled = false -let consoleSprite = new Sprite(canvasCtx, "console.png", canvas.width/2, 554, 480, 86) -let syntheSprite = new Sprite(canvasCtx, "synthe.png", canvas.width/2, 540, 110, 80) -let canonSprites = [] -for (let note=FIRST_NOTE; note canonSprite.draw()) - noteSprites.forEach(noteSprite => noteSprite.draw()) - syntheSprite.draw() - explosionSprites.forEach(explosionSprite => explosionSprite.draw()) -} - - let audioCtx let volume let wave @@ -282,19 +285,32 @@ function init() { onpartialinput() + Tone.Transport.scheduleRepeat(draw, DRAW_PERIOD) + Tone.Transport.scheduleRepeat(update, UPDATE_PERIOD) + showSettings() } - startDialog.onclose = init +function draw() { + canvasCtx.clearRect(0, 0, canvas.width, canvas.height) + + consoleSprite.draw() + cannonSprites.forEach(cannonSprite => cannonSprite.draw()) + noteSprites.forEach(noteSprite => noteSprite.draw()) + syntheSprite.draw() + explosionSprites.forEach(explosionSprite => explosionSprite.draw()) +} + function showSettings() { pause() settingsDialog.showModal() } +window.onblur = showSettings function pause() { Tone.Transport.pause() - window.clearInterval(updateTaskId) + //window.clearInterval(updateTaskId) playing = false } @@ -302,24 +318,14 @@ settingsButton.onclick = showSettings keyMapInput.onchange = function(event) { keyMap = keyMapInput.value -} - -volRange.oninput = function(event) { - volume.gain.linearRampToValueAtTime(volRange.value, audioCtx.currentTime) -} - -modRange.oninput = function(event) { - depth.gain.value = modRange.value -} - -function onpartialinput() { - wave = audioCtx.createPeriodicWave( - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0].concat(Array.from(document.querySelectorAll(".partial")).map(range => range.value)), - {disableNormalization: false,} - ) - for (const note in oscillators) { - oscillators[note].setPeriodicWave(wave) + if (midiKeyboard) { + cannonSprites.forEach((cannonSprite, index) => { + cannonSprite.key = NOTE_NAMES[index % NOTE_NAMES.length] + }) + } else { + cannonSprites.forEach((cannonSprite, index) => { + cannonSprite.key = keyMap[index - FIRST_NOTE].toUpperCase() + }) } } @@ -346,22 +352,44 @@ midiSelect.onfocus = function() { ) } +var midiKeyboard = "" midiSelect.oninput = () => { for (const id in midiIputs) midiIputs[id].onmidimessage = null - if (midiSelect.value) { - midiIputs[midiSelect.value].onmidimessage = onMIDIMessage + midiKeyboard = midiSelect.value + if (midiKeyboard) { + midiIputs[midiKeyboard].onmidimessage = onMIDIMessage + } + keyMapInput.onchange() +} + +volRange.oninput = function(event) { + volume.gain.linearRampToValueAtTime(volRange.value, audioCtx.currentTime) +} + +modRange.oninput = function(event) { + depth.gain.value = modRange.value +} + +function onpartialinput() { + wave = audioCtx.createPeriodicWave( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0].concat(Array.from(document.querySelectorAll(".partial")).map(range => range.value)), + {disableNormalization: false,} + ) + for (const note in oscillators) { + oscillators[note].setPeriodicWave(wave) } } function onMIDIMessage(event) { let [code, note, velocity] = event.data - if (144 <= code && code <= 159 && canonSprites[note]) { + if (144 <= code && code <= 159 && cannonSprites[note]) { //playNote(note, velocity / 128) - canonSprites[note].shooting = true - } else if (128 <= code && code <= 143 && canonSprites[note]) { + cannonSprites[note].shooting = true + } else if (128 <= code && code <= 143 && cannonSprites[note]) { //stopNote(note) - canonSprites[note].shooting = false + cannonSprites[note].shooting = false } } @@ -377,21 +405,23 @@ function newGame() { let midiSong let noteSprites = [] let explosionSprites = [] +let speed async function nextLevel() { level++ midiSong = await Midi.fromUrl(`midi/${level}.mid`) levelTitle.innerText = `Niveau ${level}` songNameTitle.innerText = midiSong.name + speed = 0.04 * FLOOR / midiSong.header.tempos[0].bpm noteSprites = [] midiSong.tracks.forEach(track => { //console.log(track.name) track.notes.filter(note => FIRST_NOTE <= note.midi && note.midi <= LAST_NOTE).forEach(note => { let noteSprite let durationInQuarter = note.durationTicks / midiSong.header.ppq - if (durationInQuarter <= 0.25) noteSprite = new Sixteenth(canvasCtx, note.midi, 1000*note.duration) - else if (durationInQuarter <= 0.5) noteSprite = new Eighth(canvasCtx, note.midi, 1000*note.duration) - else if (durationInQuarter <= 1) noteSprite = new Quarter(canvasCtx, note.midi, 1000*note.duration) - else noteSprite = new Whole(canvasCtx, note.midi, 1000*note.duration) + if (durationInQuarter <= 0.25) noteSprite = new Sixteenth(canvasCtx, note.midi, note.duration) + else if (durationInQuarter <= 0.5) noteSprite = new Eighth(canvasCtx, note.midi, note.duration) + else if (durationInQuarter <= 1) noteSprite = new Quarter(canvasCtx, note.midi, note.duration) + else noteSprite = new Whole(canvasCtx, note.midi, note.duration) Tone.Transport.scheduleOnce(time => noteSprites.push(noteSprite), note.time) }) }) @@ -406,43 +436,42 @@ let updateTaskId function resume() { playing = true Tone.Transport.start() - updateTaskId = window.setInterval(update, UPDATE_PERIOD) } function update() { noteSprites.forEach(noteSprite => { - noteSprite.y += 0.5 + noteSprite.y += speed }) - noteSprites.filter(noteSprite => noteSprite.y >= 420).forEach(noteSprite => { + noteSprites.filter(noteSprite => noteSprite.y >= FLOOR).forEach(noteSprite => { stopNote(noteSprite.note) let explosionSprite = noteSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) }) - noteSprites = noteSprites.filter(note => note.y < 420) + noteSprites = noteSprites.filter(note => note.y < FLOOR) - canonSprites.forEach(canonSprite => { - let noteSprite = noteSprites.find(noteSprite => noteSprite.note == canonSprite.note) + cannonSprites.forEach(cannonSprite => { + let noteSprite = noteSprites.find(noteSprite => noteSprite.note == cannonSprite.note) if (noteSprite) { - noteSprite.shot = canonSprite.shooting + noteSprite.shot = cannonSprite.shooting if (noteSprite.shot) { noteSprite.duration -= UPDATE_PERIOD if (noteSprite.duration > 0) { - playNote(canonSprite.note) - canonSprite.impactY = noteSprite.y + playNote(cannonSprite.note) + cannonSprite.impactY = noteSprite.y } else { - stopNote(canonSprite.note) + stopNote(cannonSprite.note) let explosionSprite = noteSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) noteSprites.remove(noteSprite) } } else { - stopNote(canonSprite.note) + stopNote(cannonSprite.note) } } else { - stopNote(canonSprite.note) - canonSprite.impactY = 0 + stopNote(cannonSprite.note) + cannonSprite.impactY = 0 } }) } @@ -471,6 +500,8 @@ function stopNote(note) { velocity = oscillators[note].velocity.gain.value oscillators[note].velocity.gain.setValueCurveAtTime([velocity, velocity/10, velocity/20, 0], audioCtx.currentTime + 0.1, 0.5) + oscillators[note].stop(audioCtx.currentTime + 0.6) + delete(oscillators[note]) } @@ -495,13 +526,14 @@ function playNoise(noiseDuration, startGain=0.5, bandHz=1000) { gain.gain.setValueCurveAtTime([startGain, startGain/5, 0], audioCtx.currentTime, noiseDuration) noise.connect(bandpass).connect(gain).connect(audioCtx.destination) noise.start() + noise.stop(audioCtx.currentTime + noiseDuration) } document.onkeydown = function(event) { if (playing && keyMap.includes(event.key)) { event.preventDefault() let note = FIRST_NOTE + keyMap.indexOf(event.key) - canonSprites[note].shooting = true + cannonSprites[note].shooting = true } } @@ -509,6 +541,6 @@ document.onkeyup = function(event) { if (playing && keyMap.includes(event.key)) { event.preventDefault() let note = FIRST_NOTE + keyMap.indexOf(event.key) - canonSprites[note].shooting = false + cannonSprites[note].shooting = false } } \ No newline at end of file diff --git a/img/cannon.png b/img/cannon.png new file mode 100644 index 0000000..b4b6656 Binary files /dev/null and b/img/cannon.png differ diff --git a/img/canon.png b/img/canon.png index f246812..136f05b 100644 Binary files a/img/canon.png and b/img/canon.png differ diff --git a/img/pipe.png b/img/pipe.png new file mode 100644 index 0000000..ff5d357 Binary files /dev/null and b/img/pipe.png differ diff --git a/midi/1.mid b/midi/1.mid index b2527ce..a094bab 100644 Binary files a/midi/1.mid and b/midi/1.mid differ