const MAX_LEVEL = 3 const DRAW_PERIOD = 0.04 // s const FLOOR = 390 // px const TIME_TO_SCREEN = 10 // s const UPDATE_PERIOD = 0.01 // s const STEP = FLOOR * UPDATE_PERIOD / TIME_TO_SCREEN // px const FIRST_NOTE = 48 // C2 const LAST_NOTE = 73 // C4 const NOTE_NAMES = [ "C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B" ] const FREQUENCIES = [ 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, ] Array.prototype.remove = function(item) { let index = this.indexOf(item) if (index == 0) { this.shift() return true } else if (index > 0) { this.splice(index, index) return true } else return false } class Sprite { constructor(canvasCtx, src, x, y, width, height, frames=1, zoom=2) { this.canvasCtx = canvasCtx this.sprite = new Image() this.sprite.src = `img/${src}` this.sx = 0 this.sy = 0 this.sWidth = width this.sHeight = height this.dWidth = zoom * width this.dHeight = zoom * height this.x = x this.y = y this.frames = frames this.frame = 0 this.animated = false this.repeat = false } set x(x) { this._x = x this.dx = x - (this.dWidth / 2) } get x() { return this._x } set y(y) { this._y = y this.dy = y - (this.dHeight / 2) } get y() { return this._y } play(repeat=false) { this.animated = true this.repeat = repeat return new Promise((resolve, reject) => { this.onanimationend = resolve }) } animate() { if (this.animated) { if (this.repeat) { this.frame = (this.frame + 1) % this.frames } else { if (this.frame < this.frames -1) { this.frame++ } else { this.onanimationend() } } } } onanimationend () {} draw(deltaX=0, deltaY=0) { canvasCtx.drawImage( this.sprite, this.sx + this.frame * this.sWidth, this.sy, this.sWidth, this.sHeight, this.dx + deltaX, this.dy + deltaY, this.dWidth, this.dHeight ) this.animate() } } class Cannon extends Sprite { constructor(canvasCtx, note) { let sharp = [1, 3, 6, 8, 10].includes(note % 12) 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(deltaX=0, deltaY=0) { if (this.shooting) { this.frame += this.df if (this.frame >= this.frames) this.df = -1 else if (this.frame <= 2) this.df = 1 } else { if (this.frame > 0) this.frame-- } this.pipeSprite.draw(deltaX, deltaY) this.canvasCtx.fillStyle = "#d3d6cf" this.canvasCtx.fillText(this.key, this.pipeSprite.x+2, this.pipeSprite.y+9) this.canvasCtx.fillStyle = "#222327" this.canvasCtx.fillText(this.key, this.pipeSprite.x, this.pipeSprite.y+8) super.draw(deltaX, deltaY) 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) if (this.impactY) this.canvasCtx.drawImage(this.sprite, this.sWidth*(this.frame), 2*this.sHeight, this.sWidth, this.impactHeight, this.dx, this.impactY, this.dWidth, 2*this.impactHeight) } } explose() { return new Sprite(canvasCtx, "big-explosion.png", this.x, this.y, 48, 48, 8) } } class Note extends Sprite { constructor(canvasCtx, note, duration, velocity, sx, sy, width, height, frames, shotAnimationPeriod) { super(canvasCtx, "note.png", 34 * (note - FIRST_NOTE) + 66, 0, width, height, frames, 1) this.note = note this.duration = duration this.velocity = velocity this.sx = sx this.sy = sy this.shotAnimationPeriod = shotAnimationPeriod this.shot = false this.time = 0 } animate() { this.frame = Math.floor(this.time/10) % this.frames this.time++ } draw(deltaX=0, deltaY=0) { if (this.shot) { this.drawShot() } else { super.draw(deltaX, deltaY) } } drawShot() { canvasCtx.drawImage(this.sprite, 0, 0, this.sWidth, this.sHeight, this.dx, this.dy, this.dWidth, this.dHeight) } explose() { return new Sprite(this.canvasCtx, "tiny-explosion.png", this.x, this.y, 16, 16, 7) } playNoise(time) { playNoise(0.4, 1400, 0.3, time) } } class Sixteenth extends Note { constructor(canvasCtx, note, duration, velocity) { super(canvasCtx, note, duration, velocity, 21, 0, 21, 32, 2, 2) } } class Eighth extends Note { constructor(canvasCtx, note, duration, velocity) { super(canvasCtx, note, duration, velocity, 42, 0, 21, 32, 2, 2) } } class Quarter extends Note { constructor(canvasCtx, note, duration, velocity) { super(canvasCtx, note, duration, velocity, 34, 33, 30, 66, 2, 4) } drawShot() { canvasCtx.drawImage(this.sprite, 0, 34, 35, 66, this.dx, this.dy, 35, 66) } explose() { return new Sprite(this.canvasCtx, "little-explosion.png", this.x, this.y, 33, 33, 5) } playNoise(time) { playNoise(0.5, 1000, 0.5, time) } } class Whole extends Note { constructor(canvasCtx, note, duration, velocity) { super(canvasCtx, note, duration, velocity, 36, 100, 36, 40, 1) } animate() {} drawShot() { canvasCtx.drawImage(this.sprite, 0, this.sy, this.sWidth, this.sHeight, this.dx, this.dy, this.dWidth, this.dHeight) } explose() { return new Sprite(canvasCtx, "big-explosion.png", this.x, this.y, 48, 48, 8) } playNoise(time) { playNoise(0.7, 400, 0.8, time) } } let keyMap = keyMapInput.value let playing = false let canvasCtx = canvas.getContext("2d") canvasCtx.mozImageSmoothingEnabled = false canvasCtx.webkitImageSmoothingEnabled = false canvasCtx.msImageSmoothingEnabled = false canvasCtx.imageSmoothingEnabled = false canvasCtx.font = '12px "Press Start 2P"' canvasCtx.textAlign = "center" let consoleSprite = new Sprite(canvasCtx, "console.png", canvas.width/2, 554, 482, 86) let syntheSprite = new Sprite(canvasCtx, "synthe.png", canvas.width/2, 546, 110, 80) let cannonSprites = [] for (let note=FIRST_NOTE; note 5) { animateConsole = 0 } } let deltaY = animateConsole? 4 : 0; consoleSprite.draw(0, deltaY) cannonSprites.forEach(cannonSprite => cannonSprite.draw(0, deltaY)) noteSprites.forEach(noteSprite => noteSprite.draw()) syntheSprite.draw(0, -deltaY) explosionSprites.forEach(explosionSprite => explosionSprite.draw()) batterySprite.draw() } function showSettings() { pause() settingsDialog.showModal() } window.onblur = showSettings function pause() { Tone.Transport.pause() //window.clearInterval(updateTaskId) playing = false } settingsButton.onclick = showSettings keyMapInput.onchange = function(event) { keyMap = keyMapInput.value 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() }) } } var midiIputs midiSelect.onfocus = function() { midiIputs = {} midiSelect.innerHTML = `` navigator.requestMIDIAccess().then( midiAccess => { if (midiAccess.inputs.size) { for ([,input,] of midiAccess.inputs) { midiIputs[input.id] = input var option = document.createElement("option") option.value = input.id option.innerText = input.name midiSelect.add(option) input.onmidimessage = null } } }, error => { console.log(error) } ) } var midiKeyboard = "" midiSelect.oninput = () => { for (const id in midiIputs) midiIputs[id].onmidimessage = null 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 && cannonSprites[note]) { cannonSprites[note].shooting = true } else if (128 <= code && code <= 143 && cannonSprites[note]) { cannonSprites[note].shooting = false } } let level function newGame() { level = 0 nextLevel() } let midiSong let noteSprites = [] let explosionSprites = [] let health async function nextLevel(time=0) { Tone.Transport.pause() level++ if (level <= MAX_LEVEL) { health = 12 batterySprite.frame = health midiSong = await Midi.fromUrl(`midi/${level}.mid`) levelTitle.innerText = `Niveau ${level}` songNameTitle.innerText = midiSong.name 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, 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), time + note.time) }) }) Tone.Transport.scheduleOnce(time => nextLevel(time), time + midiSong.duration + TIME_TO_SCREEN) levelDialog.showModal() } else { // win } } levelDialog.onclose = resume function resume() { settingsDialog.onclose = resume playing = true Tone.Transport.start() } function update(time) { noteSprites.filter(noteSprite => !noteSprite.shot).forEach(noteSprite => { noteSprite.y += STEP }) noteSprites.filter(noteSprite => noteSprite.y >= FLOOR).forEach(noteSprite => { let explosionSprite = noteSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) noteSprite.playNoise(time) animateConsole = 1 health-- batterySprite.frame = health }) noteSprites = noteSprites.filter(note => note.y < FLOOR) if (health <= 0) { gameOver(time) return } cannonSprites.filter(cannonSprite => cannonSprite.shooting).forEach(cannonSprite => { let noteSprite = noteSprites.find(noteSprite => noteSprite.note == cannonSprite.note) if (noteSprite) { if (!noteSprite.shot) { playNote(noteSprite.note, noteSprite.velocity, noteSprite.duration, time) cannonSprite.impactY = noteSprite.y noteSprite.shot = true window.setTimeout(() => { noteSprites.remove(noteSprite) let explosionSprite = noteSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) }, noteSprite.duration * 1000) } } else { cannonSprite.impactY = 0 } }) } function gameOver(time) { playing = false cannonSprites.forEach(cannonSprite => { let explosionSprite = cannonSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) }) noteSprites.forEach(noteSprite => { let explosionSprite = noteSprite.explose() explosionSprites.push(explosionSprite) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) }) noteSprites = [] playNoise(0.7, 400, 2, time) Tone.Transport.clear(updateTaskId) Tone.Transport.scheduleOnce((time) => { Tone.Transport.stop(time) gameOverDialog.showModal() }, time + 0.2) } gameOverDialog.onclose = () => { document.location = "" } function playNote(note, velocity=0.7, duration=0, time=0) { if(oscillators[note]) return var oscillator = audioCtx.createOscillator() oscillator.frequency.value = FREQUENCIES[note] oscillator.setPeriodicWave(wave) oscillator.velocity = audioCtx.createGain() oscillator.velocity.gain.value = 0 oscillator.velocity.gain.linearRampToValueAtTime(velocity, time + 0.05) oscillator.connect(oscillator.velocity) oscillator.start() oscillator.velocity.connect(volume) depth.connect(oscillator.detune) if (duration) { oscillator.velocity.gain.setValueCurveAtTime([velocity, velocity/10, velocity/20, 0], time + duration, 0.5) oscillator.stop(time + duration + 0.5) } else { oscillators[note] = oscillator } } function stopNote(note, time=0) { if(!oscillators[note]) return velocity = oscillators[note].velocity.gain.value oscillators[note].velocity.gain.setValueCurveAtTime([velocity, velocity/10, velocity/20, 0], time, 0.5) oscillators[note].stop(time + 0.5) delete(oscillators[note]) } function playNoise(startGain=0.5, bandHz=1000, duration, time) { const bufferSize = audioCtx.sampleRate * duration const noiseBuffer = new AudioBuffer({ length: bufferSize, sampleRate: audioCtx.sampleRate, }) const data = noiseBuffer.getChannelData(0) for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const noise = new AudioBufferSourceNode(audioCtx, { buffer: noiseBuffer, }) const bandpass = new BiquadFilterNode(audioCtx, { type: "bandpass", frequency: bandHz, }) const gain = new GainNode(audioCtx) gain.gain.setValueCurveAtTime([startGain, startGain/5, 0], time, duration) noise.connect(bandpass).connect(gain).connect(audioCtx.destination) noise.start() noise.stop(time + duration) } document.onkeydown = function(event) { if (playing && keyMap.includes(event.key)) { event.preventDefault() let note = FIRST_NOTE + keyMap.indexOf(event.key) cannonSprites[note].shooting = true } } document.onkeyup = function(event) { if (playing && keyMap.includes(event.key)) { event.preventDefault() let note = FIRST_NOTE + keyMap.indexOf(event.key) cannonSprites[note].shooting = false } }