Compare commits

...

27 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
40900f472c exponentialRampToValueAtTime 2024-09-23 19:16:22 +02:00
455f96bb3d exponentialRampToValueAtTime 2024-09-23 19:14:37 +02:00
182de5b3fc add cannon sound 2024-09-15 21:23:19 +02:00
e97c535fc6 promise 2024-08-31 01:54:50 +02:00
90df10e141 Play Him Off, Keyboard Cat 2024-08-31 01:37:27 +02:00
0a09ad4e24 victory 2024-08-30 23:47:30 +02:00
fe0e618c76 same width 2024-08-29 01:23:39 +02:00
3fac8105d2 flex direction 2024-08-28 23:40:07 +02:00
a85f092ce7 right arrow 2024-08-28 23:33:05 +02:00
0d024ad390 keyMapInput selection 2024-08-28 23:23:43 +02:00
8fe3699d70 save midiKeyboard 2024-08-28 22:56:30 +02:00
e960b48b27 settings menu 2024-08-28 20:46:38 +02:00
b0cc701226 smaller 2024-08-28 20:35:36 +02:00
22e15c6397 key select 2024-08-28 20:33:10 +02:00
406ae270b2 GAME OVER 2024-08-28 18:17:55 +02:00
1286d0b8c4 fix time to screen 2024-08-27 11:06:33 +02:00
f2b1e696bb swap songs 2024-08-27 11:06:17 +02:00
11 changed files with 285 additions and 167 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)

296
app.js
View File

@ -1,11 +1,15 @@
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 FLOOR = 386 // px
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 = [
// 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,
@ -16,7 +20,6 @@ const FREQUENCIES = [
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) {
@ -83,7 +86,7 @@ class Sprite {
} else {
if (this.frame < this.frames -1) {
this.frame++
} else {
} else if (this.frame == this.frames -1) {
this.onanimationend()
}
}
@ -92,15 +95,15 @@ class Sprite {
onanimationend () {}
draw() {
draw(deltaX=0, deltaY=0) {
canvasCtx.drawImage(
this.sprite,
this.sx + this.frame * this.sWidth,
this.sy,
this.sWidth,
this.sHeight,
this.dx,
this.dy,
this.dx + deltaX,
this.dy + deltaY,
this.dWidth,
this.dHeight
)
@ -113,9 +116,10 @@ class Sprite {
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)
//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.key = keyMap[note - FIRST_NOTE].toUpperCase()
this.key = keyMap[note - FIRST_NOTE]?.toUpperCase() || ""
this.impactHeight = 9
this.impactY = 0
this.sy = sharp? 0 : this.sHeight
@ -124,7 +128,7 @@ class Cannon extends Sprite {
this.pipeSprite = new Sprite(canvasCtx, "pipe.png", this.x-1, this.y+36, 16, 18)
}
draw() {
draw(deltaX=0, deltaY=0) {
if (this.shooting) {
this.frame += this.df
if (this.frame >= this.frames) this.df = -1
@ -132,17 +136,21 @@ class Cannon extends Sprite {
} else {
if (this.frame > 0) this.frame--
}
this.pipeSprite.draw()
this.pipeSprite.draw(deltaX, deltaY)
this.canvasCtx.fillStyle = "#d3d6cf"
this.canvasCtx.fillText(this.key, this.pipeSprite.x+2, this.pipeSprite.y+10)
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()
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)
}
}
@ -157,6 +165,7 @@ class Note extends Sprite {
this.shotAnimationPeriod = shotAnimationPeriod
this.shot = false
this.time = 0
this.angriness = 1
}
animate() {
@ -164,11 +173,11 @@ class Note extends Sprite {
this.time++
}
draw() {
draw(deltaX=0, deltaY=0) {
if (this.shot) {
this.drawShot()
} else {
super.draw()
super.draw(deltaX, deltaY)
}
}
@ -220,7 +229,8 @@ class Quarter extends Note {
class Whole extends Note {
constructor(canvasCtx, note, duration, velocity) {
super(canvasCtx, note, duration, velocity, 36, 99, 36, 40, 1)
super(canvasCtx, note, duration, velocity, 36, 100, 36, 40, 1)
this.angriness = 2
}
animate() {}
@ -251,14 +261,16 @@ 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 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 cannonSprites = []
for (let note=FIRST_NOTE; note<LAST_NOTE; note++) cannonSprites[note] = new Cannon(canvasCtx, note)
let batterySprite = new Sprite(canvasCtx, "battery.png", 890, 40, 44, 13, 13)
batterySprite.frame = 12
window.onload = function() {
draw()
startDialog.showModal()
//window.setInterval(draw, 60)
}
let audioCtx
@ -268,6 +280,7 @@ let mod
let depth
let compressor
let oscillators = {}
let updateTaskId
function init() {
Tone.start()
@ -295,35 +308,56 @@ function init() {
onpartialinput()
Tone.Transport.scheduleRepeat(draw, DRAW_PERIOD)
Tone.Transport.scheduleRepeat(update, UPDATE_PERIOD)
updateTaskId = Tone.Transport.scheduleRepeat(update, UPDATE_PERIOD)
showSettings()
if (window.localStorage.midiKeyboard) {
midiSelect.onfocus()
keyMapInput.onchange()
}
settingsDialog.onclose = newGame
pause()
}
startDialog.onclose = init
let animateConsole = 0
function draw() {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height)
consoleSprite.draw()
cannonSprites.forEach(cannonSprite => cannonSprite.draw())
if (animateConsole) {
animateConsole++
if (animateConsole > 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()
syntheSprite.draw(0, -deltaY)
explosionSprites.forEach(explosionSprite => explosionSprite.draw())
batterySprite.draw()
}
function showSettings() {
pause()
settingsDialog.showModal()
}
window.onblur = showSettings
window.onblur = pause
function pause() {
Tone.Transport.pause()
//window.clearInterval(updateTaskId)
playing = false
settingsDialog.showModal()
}
settingsButton.onclick = showSettings
settingsButton.onclick = pause
keyMapInput.onclick = keyMapInput.onkeyup = function(event) {
let cursorPosition = keyMapInput.selectionEnd
if (cursorPosition == 0 || (event.key == "ArrowRight" && cursorPosition <= keyMapInput.value.length)) {
keyMapInput.setSelectionRange(cursorPosition, cursorPosition+1)
} else {
keyMapInput.setSelectionRange(cursorPosition-1, cursorPosition)
}
}
keyMapInput.onchange = function(event) {
keyMap = keyMapInput.value
@ -339,9 +373,10 @@ keyMapInput.onchange = function(event) {
}
var midiIputs
var midiKeyboard = window.localStorage.midiKeyboard || ""
midiSelect.onfocus = function() {
midiIputs = {}
midiSelect.innerHTML = `<option value="">Aucun</option>`
midiSelect.innerHTML = `<option value="">Aucun (utiliser les touches ci-dessous)</option>`
navigator.requestMIDIAccess().then(
midiAccess => {
if (midiAccess.inputs.size) {
@ -353,6 +388,7 @@ midiSelect.onfocus = function() {
midiSelect.add(option)
input.onmidimessage = null
}
if(midiIputs[midiKeyboard]) midiSelect.value = midiKeyboard
}
},
error => {
@ -361,7 +397,6 @@ midiSelect.onfocus = function() {
)
}
var midiKeyboard = ""
midiSelect.oninput = () => {
for (const id in midiIputs) midiIputs[id].onmidimessage = null
midiKeyboard = midiSelect.value
@ -369,6 +404,7 @@ midiSelect.oninput = () => {
midiIputs[midiKeyboard].onmidimessage = onMIDIMessage
}
keyMapInput.onchange()
window.localStorage.midiKeyboard = midiKeyboard
}
volRange.oninput = function(event) {
@ -390,23 +426,8 @@ function onpartialinput() {
}
}
function onMIDIMessage(event) {
let [code, note, velocity] = event.data
if (144 <= code && code <= 159 && cannonSprites[note]) {
//playNote(note, velocity / 128)
cannonSprites[note].shooting = true
} else if (128 <= code && code <= 143 && cannonSprites[note]) {
//stopNote(note)
cannonSprites[note].shooting = false
}
}
settingsDialog.onclose = newGame
let level
function newGame() {
settingsDialog.onclose = resume
level = 0
nextLevel()
}
@ -414,58 +435,135 @@ function newGame() {
let midiSong
let noteSprites = []
let explosionSprites = []
let speed
async function nextLevel() {
let health
function nextLevel(time=Tone.Transport.seconds) {
Tone.Transport.pause(time)
level++
midiSong = await Midi.fromUrl(`midi/${level}.mid`)
levelTitle.innerText = `Niveau ${level}`
songNameTitle.innerText = midiSong.name
speed = 4 * UPDATE_PERIOD * 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, 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)
})
midiSong = Midi.fromUrl(`midi/${level}.mid`).then((midi) => {
midiSong = midi
health = 12
batterySprite.frame = health
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(nextLevel, time + midiSong.duration + TIME_TO_SCREEN)
levelDialog.showModal()
}).catch((error) => {
victory(time)
})
Tone.Transport.scheduleOnce(time => nextLevel, midiSong.duration)
levelDialog.showModal()
}
levelDialog.onclose = resume
let updateTaskId
function resume() {
settingsDialog.onclose = resume
playing = true
Tone.Transport.start()
}
document.onkeydown = function(event) {
if (event.altKey || event.ctrlKey) return
let keyIndex = keyMap.indexOf(event.key.toLowerCase())
if (keyIndex >= 0) {
if (event.target != keyMapInput) event.preventDefault()
shoot(FIRST_NOTE + keyIndex)
}
}
document.onkeyup = function(event) {
if (event.altKey || event.ctrlKey) return
if (event.key =="Escape") pause()
let keyIndex = keyMap.indexOf(event.key.toLowerCase())
if (keyIndex >= 0) {
if (event.target != keyMapInput) event.preventDefault()
stopShoot(FIRST_NOTE + keyIndex)
}
}
function onMIDIMessage(event) {
let [code, note, velocity] = event.data
if (144 <= code && code <= 159 && cannonSprites[note]) {
shoot(note)
} else if (128 <= code && code <= 143 && cannonSprites[note]) {
stopShoot(note)
}
}
function shoot(note) {
cannonSprites[note].shooting = true
if (cannonSprites[note].oscillator) return
var oscillator = audioCtx.createOscillator({type: "sawtooth"})
oscillator.type = "sawtooth"
oscillator.frequency.value = FREQUENCIES[note - 24]
oscillator.velocity = audioCtx.createGain()
oscillator.velocity.gain.value = 0
oscillator.velocity.gain.linearRampToValueAtTime(0.15, Tone.Transport.seconds + 0.05)
oscillator.connect(oscillator.velocity)
oscillator.start()
oscillator.velocity.connect(volume)
depth.connect(oscillator.detune)
cannonSprites[note].oscillator = oscillator
}
function stopShoot(note) {
cannonSprites[note].shooting = false
if (!cannonSprites[note].oscillator) return
var oscillator = cannonSprites[note].oscillator
oscillator.velocity.gain.exponentialRampToValueAtTime(0.01, Tone.Transport.seconds, 0.5)
oscillator.stop(Tone.Transport.seconds + 0.5)
delete(cannonSprites[note].oscillator)
}
function update(time) {
noteSprites.forEach(noteSprite => {
noteSprite.y += speed
noteSprites.filter(noteSprite => !noteSprite.shot).forEach(noteSprite => {
noteSprite.y += STEP
})
noteSprites.filter(noteSprite => noteSprite.y >= FLOOR).forEach(noteSprite => {
stopNote(noteSprite.note)
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)
cannonSprite.impactY = noteSprite?.y || 0
if (noteSprite) {
cannonSprite.impactY = noteSprite.y
if (!noteSprite.shot) {
playNote(noteSprite.note, noteSprite.velocity, noteSprite.duration, time)
cannonSprite.impactY = noteSprite.y
noteSprite.shot = true
window.setTimeout(() => {
noteSprites.remove(noteSprite)
@ -480,7 +578,7 @@ function update(time) {
})
}
function playNote(note, velocity=0.7, duration=0, time=0) {
function playNote(note, velocity=0.7, duration=0, time=audioCtx.currentTime) {
if(oscillators[note]) return
var oscillator = audioCtx.createOscillator()
@ -496,19 +594,14 @@ function playNote(note, velocity=0.7, duration=0, time=0) {
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
}
oscillators[note] = oscillator
if (duration) stopNote(note, time + duration)
}
function stopNote(note, time=0) {
function stopNote(note, time=audioCtx.currentTime) {
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].velocity.gain.exponentialRampToValueAtTime(0.01, time + 0.5)
oscillators[note].stop(time + 0.5)
delete(oscillators[note])
@ -532,24 +625,35 @@ function playNoise(startGain=0.5, bandHz=1000, duration, time) {
frequency: bandHz,
})
const gain = new GainNode(audioCtx)
gain.gain.setValueCurveAtTime([startGain, startGain/5, 0], time, duration)
gain.gain.value = startGain
gain.gain.exponentialRampToValueAtTime(0.01, 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
}
function victory(time) {
canvas.classList = "victory"
victoryDialog.showModal()
}
document.onkeyup = function(event) {
if (playing && keyMap.includes(event.key)) {
event.preventDefault()
let note = FIRST_NOTE + keyMap.indexOf(event.key)
cannonSprites[note].shooting = false
}
function gameOver(time) {
playing = false
cannonSprites.forEach(cannonSprite => {
let explosionSprite = cannonSprite.explose()
explosionSprites.push(explosionSprite)
explosionSprite.play().then(() => explosionSprites.remove(explosionSprite))
})
playNoise(0.7, 400, 2, time)
Tone.Transport.clear(updateTaskId)
Tone.Transport.scheduleOnce((time) => {
Tone.Transport.stop(time)
}, time + 0.1)
gameOverDialog.showModal()
}
victoryDialog.onclose = gameOverDialog.onclose = function() {
document.location = ""
}

BIN
img/battery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

BIN
img/fireworks.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="fr">
<html lang='fr' dir="ltr" prefix="og: https://ogp.me/ns#">
<head>
<meta charset="utf-8">
<title>Angry notes from outer space</title>
<title>🎶🎶🎶</title>
<link rel="icon" href="img/favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
@ -11,10 +11,19 @@
<link href="style.css" rel="stylesheet">
<script src="https://unpkg.com/tone" ></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>
<body>
<section class="nes-container is-dark" style="padding: 0;">
<canvas style="background-image: url(img/pixel-city-chill.gif); background-size: cover;" id="canvas" width="960" height="540" tabindex="9"></canvas>
<canvas id="canvas" width="960" height="540" tabindex="9"></canvas>
</section>
<dialog id="startDialog" class="nes-dialog is-rounded is-dark">
<form method="dialog">
@ -35,21 +44,20 @@
<form method="dialog">
<h2 class="title is-centered">Options</h2>
<section class="nes-container with-title is-dark">
<h3 class="title">Contrôles</h3>
<div class="nes-field">
<label for="keyMapInput">Clavier d'ordinateur</label>
<div style="overflow-x: scroll;padding: 2px;">
<input type="text" id="keyMapInput" class="nes-input is-dark"
minlength="25" maxlength="25" size="25" required
placeholder="wsxdcvgbhnj,e'r(tyèu_içop" value="wsxdcvgbhnj,e'r(tyèu_içop"/>
</div>
</div>
<label for="midiSelect" style="color:#fff">Clavier MIDI</label>
<h3 class="title">Clavier MIDI</h3>
<div class="nes-select is-dark">
<select id="midiSelect">
<option value="">Aucun</option>
<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 style="overflow-x: scroll;padding: 2px;">
<input type="text" id="keyMapInput" class="nes-textarea is-dark"
minlength="25" maxlength="25" size="25" required tabindex="2"
title="Cliquez pour changer une touche"
placeholder="ccddeffggaabccddeffggaabc" value="wsexdrcftvgybhunji,ko;lp:"/>
</div>
</div>
</section>
<section class="nes-container with-title is-dark">
<h3 class="title">Son</h3>
@ -69,7 +77,7 @@
</div>
</section>
<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>
</form>
</dialog>
@ -78,7 +86,24 @@
<h2 id="levelTitle" class="title is-centered">Niveau X</h2>
<h3 id="songNameTitle" class="title is-centered">Titre</h3>
<div class="is-centered">
<button class="nes-btn is-primary" tabindex="1">Jouer</button>
<button class="nes-btn is-primary" autofocus>Jouer</button>
</div>
</form>
</dialog>
<dialog id="victoryDialog" class="nes-dialog is-rounded is-dark">
<form method="dialog">
<h2 class="title is-centered">Victoire !</h2>
<p>Vous avez vaincu la musique.</p>
<div class="is-centered">
<button class="nes-btn is-primary" autofocus>Rejouer ?</button>
</div>
</form>
</dialog>
<dialog id="gameOverDialog" class="nes-dialog is-rounded is-dark">
<form method="dialog">
<h2 class="title is-centered">Game over</h2>
<div class="is-centered">
<button class="nes-btn is-primary" autofocus>Rejouer ?</button>
</div>
</form>
</dialog>

Binary file not shown.

Binary file not shown.

BIN
midi/4.mid Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@ html, body, pre, code, kbd, samp {
body {
background-color: #212529;
font-size: 12;
font-size: 12px;
display: flex;
align-content: center;
height: 100vh;
@ -27,12 +27,22 @@ body::before,
pointer-events: none;
}
#canvas {
background-image: url(img/pixel-city-chill.gif);
background-size: cover;
}
#canvas.victory {
background-image: url(img/fireworks.gif);
background-size: contain;
}
.nes-dialog {
max-width: 90%;
}
h1 {
text-shadow: 0 4px #adafbc;
text-shadow: 0 3px #adafbc;
}
.is-centered {
@ -47,11 +57,22 @@ h1 {
color: white;
text-shadow: #444 0 2px, #444 2px 0, #444 0 -2px, #444 -2px 0;
text-transform: uppercase;
padding-left: 3px;
padding-left: 4px;
padding-right: 0;
letter-spacing: 4px;
width: calc(25em + 108px);
height: 54px;
font-size: 16px;
caret-color: transparent;
}
#keyMapInput::selection {
color: #209cee;
}
#keyMapInput,
input[type=range]{
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAzElEQVRYR+2X0Q6AIAhF5f8/2jYXZkwEjNSVvVUjDpcrGgT7FUkI2D9xRfQETwNIiWO85wfINfQUEyxBG2ArsLwC0jioGt5zFcwF4OYDPi/mBYKm4t0U8ATgRm3ThFoAqkhNgWkA0jJLvaOVSs7j3qMnSgXWBMiWPXe94QqMBMBc1VZIvaTu5u5pQewq0EqNZvIEMCmxAawK0DNkay9QmfFNAJUXfgGgUkLaE7j/h8fnASkxHTz0DGIBMCnBeeM7AArpUd3mz2x3C7wADglA8BcWMZhZAAAAAElFTkSuQmCC) 14 0, pointer;
}
.sliders {
@ -145,7 +166,20 @@ svg {
height: 32px;
}
.nes-balloon, .nes-balloon.is-dark, .nes-btn, .nes-container.is-rounded, .nes-container.is-rounded.is-dark, .nes-dialog.is-rounded, .nes-dialog.is-rounded.is-dark, .nes-progress, .nes-progress.is-rounded, .nes-table.is-bordered, .nes-table.is-dark.is-bordered, .nes-input, .nes-textarea, .nes-select select {
.nes-container,
.nes-balloon,
.nes-balloon.is-dark,.nes-btn,
.nes-container.is-rounded,
.nes-container.is-rounded.is-dark,
.nes-dialog.is-rounded,
.nes-dialog.is-rounded.is-dark,
.nes-progress,
.nes-progress.is-rounded,
.nes-table.is-bordered,
.nes-table.is-dark.is-bordered,
.nes-input,
.nes-textarea,
.nes-select select {
border-style: solid;
border-width: 2px;
border-image-outset: 2;

View File

@ -1,50 +0,0 @@
<body>
<label for="duration">Duration</label>
<input
name="duration"
id="duration"
type="range"
min="0"
max="2"
value="1"
step="0.1" />
<label for="band">Band</label>
<input
name="band"
id="band"
type="range"
min="400"
max="1200"
value="1000"
step="5" />
</body>
<script>
const audioCtx = new AudioContext();
function playNoise(noiseDuration, bandHz=1000) {
const bufferSize = audioCtx.sampleRate * noiseDuration
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([0.5, 1/10, 0], audioCtx.currentTime, noiseDuration)
noise.connect(bandpass).connect(gain).connect(audioCtx.destination)
noise.start()
}
</script>