Compare commits

...

10 Commits

Author SHA1 Message Date
3cb6749034 Add readme 2025-05-21 10:17:15 +02:00
7d75324835 meta 2025-05-09 18:14:24 +02:00
605ccb2893 🎶🎶🎵🎶 2025-02-23 13:21:34 +01:00
629999e44d add toccata 2025-01-24 10:34:33 +01:00
2e7c4e7c19 autofocus 2024-10-04 17:16:03 +02:00
f096b13c65 enhancements 2024-09-25 01:11:30 +02:00
69c16647cf accordeon buttons 2024-09-23 22:02:57 +02:00
ad05712e20 remove comments 2024-09-23 21:37:57 +02:00
18b03a4975 Events scheduled inside of scheduled callbacks should use the passed in scheduling time 2024-09-23 19:29:55 +02:00
257a354be7 exponentialRampToValueAtTime 2024-09-23 19:26:42 +02:00
4 changed files with 59 additions and 54 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Angry Notes From Outer Space
Blast angry notes from outer space with your MIDI keyboard !
![screenshot](https://git.malingrey.fr/adrien/angry-notes/raw/branch/main/thumbnail.png)

73
app.js
View File

@ -116,7 +116,8 @@ class Sprite {
class Cannon extends Sprite { class Cannon extends Sprite {
constructor(canvasCtx, note) { constructor(canvasCtx, note) {
let sharp = [1, 3, 6, 8, 10].includes(note % 12) let sharp = [1, 3, 6, 8, 10].includes(note % 12)
super(canvasCtx, "cannon.png", 34 * (note - FIRST_NOTE) + 66, sharp? 418:424, 11, 26, 4) //super(canvasCtx, "cannon.png", 34 * (note - FIRST_NOTE) + 66, sharp? 418:424, 11, 26, 4)
super(canvasCtx, "cannon.png", 34 * (note - FIRST_NOTE) + 66, 424 - 8*(note % 3), 11, 26, 4)
this.note = note this.note = note
this.key = keyMap[note - FIRST_NOTE]?.toUpperCase() || "" this.key = keyMap[note - FIRST_NOTE]?.toUpperCase() || ""
this.impactHeight = 9 this.impactHeight = 9
@ -260,7 +261,7 @@ canvasCtx.imageSmoothingEnabled = false
canvasCtx.font = '12px "Press Start 2P"' canvasCtx.font = '12px "Press Start 2P"'
canvasCtx.textAlign = "center" canvasCtx.textAlign = "center"
let consoleSprite = new Sprite(canvasCtx, "console.png", canvas.width/2, 554, 482, 86) let consoleSprite = new Sprite(canvasCtx, "console.png", canvas.width/2, 554, 482, 104)
let syntheSprite = new Sprite(canvasCtx, "synthe.png", canvas.width/2, 546, 110, 80) let syntheSprite = new Sprite(canvasCtx, "synthe.png", canvas.width/2, 546, 110, 80)
let cannonSprites = [] let cannonSprites = []
for (let note=FIRST_NOTE; note<LAST_NOTE; note++) cannonSprites[note] = new Cannon(canvasCtx, note) for (let note=FIRST_NOTE; note<LAST_NOTE; note++) cannonSprites[note] = new Cannon(canvasCtx, note)
@ -315,7 +316,7 @@ function init() {
} }
settingsDialog.onclose = newGame settingsDialog.onclose = newGame
showSettings() pause()
} }
startDialog.onclose = init startDialog.onclose = init
@ -339,22 +340,19 @@ function draw() {
batterySprite.draw() batterySprite.draw()
} }
function showSettings() { window.onblur = pause
pause()
settingsDialog.showModal()
}
window.onblur = showSettings
function pause() { function pause() {
Tone.Transport.pause() Tone.Transport.pause()
playing = false playing = false
settingsDialog.showModal()
} }
settingsButton.onclick = showSettings settingsButton.onclick = pause
keyMapInput.onclick = keyMapInput.onkeyup = function(event) { keyMapInput.onclick = keyMapInput.onkeyup = function(event) {
let cursorPosition = keyMapInput.selectionEnd let cursorPosition = keyMapInput.selectionEnd
if ((event.key == "ArrowRight" && cursorPosition <= keyMapInput.value.length) || cursorPosition == 0) { if (cursorPosition == 0 || (event.key == "ArrowRight" && cursorPosition <= keyMapInput.value.length)) {
keyMapInput.setSelectionRange(cursorPosition, cursorPosition+1) keyMapInput.setSelectionRange(cursorPosition, cursorPosition+1)
} else { } else {
keyMapInput.setSelectionRange(cursorPosition-1, cursorPosition) keyMapInput.setSelectionRange(cursorPosition-1, cursorPosition)
@ -438,8 +436,8 @@ let midiSong
let noteSprites = [] let noteSprites = []
let explosionSprites = [] let explosionSprites = []
let health let health
function nextLevel(time=0) { function nextLevel(time=Tone.Transport.seconds) {
Tone.Transport.pause() Tone.Transport.pause(time)
level++ level++
midiSong = Midi.fromUrl(`midi/${level}.mid`).then((midi) => { midiSong = Midi.fromUrl(`midi/${level}.mid`).then((midi) => {
midiSong = midi midiSong = midi
@ -460,7 +458,7 @@ function nextLevel(time=0) {
Tone.Transport.scheduleOnce(time => noteSprites.push(noteSprite), time + note.time) Tone.Transport.scheduleOnce(time => noteSprites.push(noteSprite), time + note.time)
}) })
}) })
Tone.Transport.scheduleOnce(time => nextLevel(time), time + midiSong.duration + TIME_TO_SCREEN) Tone.Transport.scheduleOnce(nextLevel, time + midiSong.duration + TIME_TO_SCREEN)
levelDialog.showModal() levelDialog.showModal()
}).catch((error) => { }).catch((error) => {
@ -477,18 +475,23 @@ function resume() {
} }
document.onkeydown = function(event) { document.onkeydown = function(event) {
if (playing && keyMap.includes(event.key)) { if (event.altKey || event.ctrlKey) return
event.preventDefault()
let note = FIRST_NOTE + keyMap.indexOf(event.key) let keyIndex = keyMap.indexOf(event.key.toLowerCase())
shoot(note) if (keyIndex >= 0) {
if (event.target != keyMapInput) event.preventDefault()
shoot(FIRST_NOTE + keyIndex)
} }
} }
document.onkeyup = function(event) { document.onkeyup = function(event) {
if (playing && keyMap.includes(event.key)) { if (event.altKey || event.ctrlKey) return
event.preventDefault() if (event.key =="Escape") pause()
let note = FIRST_NOTE + keyMap.indexOf(event.key)
stopShoot(note) let keyIndex = keyMap.indexOf(event.key.toLowerCase())
if (keyIndex >= 0) {
if (event.target != keyMapInput) event.preventDefault()
stopShoot(FIRST_NOTE + keyIndex)
} }
} }
@ -528,8 +531,7 @@ function stopShoot(note) {
if (!cannonSprites[note].oscillator) return if (!cannonSprites[note].oscillator) return
var oscillator = cannonSprites[note].oscillator var oscillator = cannonSprites[note].oscillator
velocity = oscillator.velocity.gain.value oscillator.velocity.gain.exponentialRampToValueAtTime(0.01, Tone.Transport.seconds, 0.5)
oscillator.velocity.gain.exponentialRampToValueAtTime(1, Tone.Transport.seconds, 0.5)
oscillator.stop(Tone.Transport.seconds + 0.5) oscillator.stop(Tone.Transport.seconds + 0.5)
delete(cannonSprites[note].oscillator) delete(cannonSprites[note].oscillator)
@ -576,7 +578,7 @@ function update(time) {
}) })
} }
function playNote(note, velocity=0.7, duration=0, time=Tone.Transport.seconds) { function playNote(note, velocity=0.7, duration=0, time=audioCtx.currentTime) {
if(oscillators[note]) return if(oscillators[note]) return
var oscillator = audioCtx.createOscillator() var oscillator = audioCtx.createOscillator()
@ -592,19 +594,14 @@ function playNote(note, velocity=0.7, duration=0, time=Tone.Transport.seconds) {
depth.connect(oscillator.detune) depth.connect(oscillator.detune)
if (duration) {
oscillator.velocity.gain.exponentialRampToValueAtTime(1, time + duration, 0.5)
oscillator.stop(time + duration + 0.5)
} else {
oscillators[note] = oscillator oscillators[note] = oscillator
} if (duration) stopNote(note, time + duration)
} }
function stopNote(note, time=Tone.Transport.seconds) { function stopNote(note, time=audioCtx.currentTime) {
if(!oscillators[note]) return if(!oscillators[note]) return
velocity = oscillators[note].velocity.gain.value oscillators[note].velocity.gain.exponentialRampToValueAtTime(0.01, time + 0.5)
oscillators[note].velocity.gain.exponentialRampToValueAtTime(1, time, 0.5)
oscillators[note].stop(time + 0.5) oscillators[note].stop(time + 0.5)
delete(oscillators[note]) delete(oscillators[note])
@ -628,7 +625,8 @@ function playNoise(startGain=0.5, bandHz=1000, duration, time) {
frequency: bandHz, frequency: bandHz,
}) })
const gain = new GainNode(audioCtx) const gain = new GainNode(audioCtx)
gain.gain.exponentialRampToValueAtTime(0.5, time, duration) gain.gain.value = startGain
gain.gain.exponentialRampToValueAtTime(0.01, time, duration)
noise.connect(bandpass).connect(gain).connect(audioCtx.destination) noise.connect(bandpass).connect(gain).connect(audioCtx.destination)
noise.start() noise.start()
noise.stop(time + duration) noise.stop(time + duration)
@ -645,16 +643,9 @@ function gameOver(time) {
cannonSprites.forEach(cannonSprite => { cannonSprites.forEach(cannonSprite => {
let explosionSprite = cannonSprite.explose() let explosionSprite = cannonSprite.explose()
explosionSprites.push(explosionSprite) explosionSprites.push(explosionSprite)
explosionSprite.play().then(() => {console.log(cannonSprite.note); explosionSprites.remove(explosionSprite)})
})
playNoise(0.7, 400, 2, time)
/*noteSprites.forEach(noteSprite => {
let explosionSprite = noteSprite.explose()
explosionSprites.push(explosionSprite)
explosionSprite.play().then(() => explosionSprites.remove(explosionSprite)) explosionSprite.play().then(() => explosionSprites.remove(explosionSprite))
}) })
noteSprites = []*/ playNoise(0.7, 400, 2, time)
Tone.Transport.clear(updateTaskId) Tone.Transport.clear(updateTaskId)
Tone.Transport.scheduleOnce((time) => { Tone.Transport.scheduleOnce((time) => {

View File

@ -1,8 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang='fr' dir="ltr" prefix="og: https://ogp.me/ns#">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Angry notes from outer space</title> <title>🎶🎶🎶</title>
<link rel="icon" href="img/favicon.png"> <link rel="icon" href="img/favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin=""> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
@ -11,6 +11,15 @@
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="https://unpkg.com/tone" ></script> <script src="https://unpkg.com/tone" ></script>
<script src="https://unpkg.com/@tonejs/midi" ></script> <script src="https://unpkg.com/@tonejs/midi" ></script>
<meta property="og:title" content="🎶Angry notes from outer space🎶"/>
<meta property="og:type" content="game"/>
<meta property="og:url" content="https://adrien.malingrey.fr/jeux/angry-notes/"/>
<meta property="og:image" content="https://adrien.malingrey.fr/jeux/angry-notes/thumbnail.png"/>
<meta property="og:image:width" content="149"/>
<meta property="og:image:height" content="108"/>
<meta property="og:description" content="Jean-Michel, les notes furieuses de l'espace attaquent&nbsp;! Détruisez-les à l'aide de votre harpe laser."/>
<meta property="og:locale" content="fr_FR"/>
<meta property="og:site_name" content="adrien.malingrey.fr"/>
</head> </head>
<body> <body>
<section class="nes-container is-dark" style="padding: 0;"> <section class="nes-container is-dark" style="padding: 0;">
@ -36,17 +45,17 @@
<h2 class="title is-centered">Options</h2> <h2 class="title is-centered">Options</h2>
<section class="nes-container with-title is-dark"> <section class="nes-container with-title is-dark">
<h3 class="title">Clavier MIDI</h3> <h3 class="title">Clavier MIDI</h3>
<div class="nes-select is-dark">
<select id="midiSelect" tabindex="3">
<option value="">Aucun (utiliser les touches ci-dessous)</option>
</select>
</div>
<div class="nes-field" style="display: flex; flex-direction: column-reverse;"> <div class="nes-field" style="display: flex; flex-direction: column-reverse;">
<div style="overflow-x: scroll;padding: 2px;"> <div style="overflow-x: scroll;padding: 2px;">
<input type="text" id="keyMapInput" class="nes-textarea is-dark" <input type="text" id="keyMapInput" class="nes-textarea is-dark"
minlength="25" maxlength="25" size="25" required tabindex="2" minlength="25" maxlength="25" size="25" required tabindex="2"
title="Cliquez pour changer une touche" title="Cliquez pour changer une touche"
placeholder="wsxdcvgbhnj,e'r(tyèu_içop" value="wsxdcvgbhnj,e'r(tyèu_içop"/> placeholder="ccddeffggaabccddeffggaabc" value="wsexdrcftvgybhunji,ko;lp:"/>
</div>
<div class="nes-select is-dark">
<select id="midiSelect" tabindex="3">
<option value="">Aucun (utiliser les touches ci-dessous)</option>
</select>
</div> </div>
</div> </div>
</section> </section>
@ -68,7 +77,7 @@
</div> </div>
</section> </section>
<menu class="is-centered"> <menu class="is-centered">
<button id="playButton" class="nes-btn is-primary" tabindex="1">OK</button> <button id="playButton" class="nes-btn is-primary" autofocus>OK</button>
</menu> </menu>
</form> </form>
</dialog> </dialog>
@ -77,7 +86,7 @@
<h2 id="levelTitle" class="title is-centered">Niveau X</h2> <h2 id="levelTitle" class="title is-centered">Niveau X</h2>
<h3 id="songNameTitle" class="title is-centered">Titre</h3> <h3 id="songNameTitle" class="title is-centered">Titre</h3>
<div class="is-centered"> <div class="is-centered">
<button class="nes-btn is-primary" tabindex="1">Jouer</button> <button class="nes-btn is-primary" autofocus>Jouer</button>
</div> </div>
</form> </form>
</dialog> </dialog>
@ -86,7 +95,7 @@
<h2 class="title is-centered">Victoire !</h2> <h2 class="title is-centered">Victoire !</h2>
<p>Vous avez vaincu la musique.</p> <p>Vous avez vaincu la musique.</p>
<div class="is-centered"> <div class="is-centered">
<button class="nes-btn is-primary" tabindex="1">Rejouer ?</button> <button class="nes-btn is-primary" autofocus>Rejouer ?</button>
</div> </div>
</form> </form>
</dialog> </dialog>
@ -94,7 +103,7 @@
<form method="dialog"> <form method="dialog">
<h2 class="title is-centered">Game over</h2> <h2 class="title is-centered">Game over</h2>
<div class="is-centered"> <div class="is-centered">
<button class="nes-btn is-primary" tabindex="1">Rejouer ?</button> <button class="nes-btn is-primary" autofocus>Rejouer ?</button>
</div> </div>
</form> </form>
</dialog> </dialog>

BIN
midi/4.mid Normal file

Binary file not shown.