fist commit
514
app.js
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
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() {
|
||||||
|
canvasCtx.drawImage(
|
||||||
|
this.sprite,
|
||||||
|
this.sx + this.frame * this.sWidth,
|
||||||
|
this.sy,
|
||||||
|
this.sWidth,
|
||||||
|
this.sHeight,
|
||||||
|
this.dx,
|
||||||
|
this.dy,
|
||||||
|
this.dWidth,
|
||||||
|
this.dHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
this.animate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Canon 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)
|
||||||
|
this.note = note
|
||||||
|
this.impactHeight = 9
|
||||||
|
this.impactY = 0
|
||||||
|
this.sy = sharp? 0 : this.sHeight
|
||||||
|
this.shooting = false
|
||||||
|
this.df = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
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--
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Note extends Sprite {
|
||||||
|
constructor(canvasCtx, note, duration, sx, sy, width, height, frames, shotAnimationPeriod) {
|
||||||
|
super(canvasCtx, "note.png", 34 * (note - FIRST_NOTE) + 66, -40, width, height, frames, 1)
|
||||||
|
this.note = note
|
||||||
|
this.duration = duration
|
||||||
|
this.sx = sx
|
||||||
|
this.sy = sy
|
||||||
|
this.shotAnimationPeriod = shotAnimationPeriod
|
||||||
|
this.shot = false
|
||||||
|
this.time = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
if (this.shot) {
|
||||||
|
this.frame = Math.floor(this.time/this.shotAnimationPeriod) % this.frames
|
||||||
|
} else {
|
||||||
|
this.frame = Math.floor(this.time/10) % this.frames
|
||||||
|
}
|
||||||
|
this.time++
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
if (this.shot && this.frame == 1) {
|
||||||
|
this.drawShot()
|
||||||
|
this.animate()
|
||||||
|
} else {
|
||||||
|
super.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawShot() {
|
||||||
|
canvasCtx.drawImage(this.sprite, 0, 0, this.sWidth, this.sHeight, this.dx, this.dy, this.dWidth, this.dHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
explose() {
|
||||||
|
playNoise(0.3, 0.4, 1400)
|
||||||
|
return new Sprite(this.canvasCtx, "tiny-explosion.png", this.x, this.y, 16, 16, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Sixteenth extends Note {
|
||||||
|
constructor(canvasCtx, note, duration) {
|
||||||
|
super(canvasCtx, note, duration, 21, 0, 21, 32, 2, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Eighth extends Note {
|
||||||
|
constructor(canvasCtx, note, duration) {
|
||||||
|
super(canvasCtx, note, duration, 42, 0, 21, 32, 2, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Quarter extends Note {
|
||||||
|
constructor(canvasCtx, note, duration) {
|
||||||
|
super(canvasCtx, note, duration, 36, 32, 28, 68, 2, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawShot() {
|
||||||
|
canvasCtx.drawImage(this.sprite, 0, 40, 34, 37, this.dx+2, this.dy+16, 34, 37)
|
||||||
|
}
|
||||||
|
|
||||||
|
explose() {
|
||||||
|
playNoise(0.5, 0.5, 1000)
|
||||||
|
return new Sprite(this.canvasCtx, "little-explosion.png", this.x, this.y, 33, 33, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Whole extends Note {
|
||||||
|
constructor(canvasCtx, note, duration) {
|
||||||
|
super(canvasCtx, note, duration, 0, 78, 36, 40, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {}
|
||||||
|
|
||||||
|
explose() {
|
||||||
|
playNoise(0.8, 0.7, 400)
|
||||||
|
return new Sprite(canvasCtx, "big-explosion.png", this.x, this.y, 48, 48, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
let canvasCtx = canvas.getContext("2d")
|
||||||
|
canvasCtx.mozImageSmoothingEnabled = false
|
||||||
|
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<LAST_NOTE; note++) canonSprites[note] = new Canon(canvasCtx, note)
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
startDialog.showModal()
|
||||||
|
window.setInterval(draw, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
canvasCtx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
consoleSprite.draw()
|
||||||
|
canonSprites.forEach(canonSprite => canonSprite.draw())
|
||||||
|
noteSprites.forEach(noteSprite => noteSprite.draw())
|
||||||
|
syntheSprite.draw()
|
||||||
|
explosionSprites.forEach(explosionSprite => explosionSprite.draw())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let audioCtx
|
||||||
|
let volume
|
||||||
|
let wave
|
||||||
|
let mod
|
||||||
|
let depth
|
||||||
|
let compressor
|
||||||
|
let oscillators = {}
|
||||||
|
function init() {
|
||||||
|
Tone.start()
|
||||||
|
|
||||||
|
audioCtx = new AudioContext()
|
||||||
|
|
||||||
|
compressor = audioCtx.createDynamicsCompressor()
|
||||||
|
compressor.threshold.setValueAtTime(-50, audioCtx.currentTime)
|
||||||
|
compressor.knee.setValueAtTime(40, audioCtx.currentTime)
|
||||||
|
compressor.ratio.setValueAtTime(12, audioCtx.currentTime)
|
||||||
|
compressor.attack.setValueAtTime(0, audioCtx.currentTime)
|
||||||
|
compressor.release.setValueAtTime(0.25, audioCtx.currentTime)
|
||||||
|
compressor.connect(audioCtx.destination)
|
||||||
|
|
||||||
|
volume = audioCtx.createGain()
|
||||||
|
volRange.oninput()
|
||||||
|
volume.connect(compressor)
|
||||||
|
|
||||||
|
mod = audioCtx.createOscillator() // the modulating oscillator
|
||||||
|
depth = audioCtx.createGain() // the modulator amplifier
|
||||||
|
modRange.oninput()
|
||||||
|
mod.frequency.value = 6
|
||||||
|
mod.connect(depth)
|
||||||
|
mod.start()
|
||||||
|
|
||||||
|
onpartialinput()
|
||||||
|
|
||||||
|
showSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
startDialog.onclose = init
|
||||||
|
|
||||||
|
function showSettings() {
|
||||||
|
pause()
|
||||||
|
settingsDialog.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause() {
|
||||||
|
Tone.Transport.pause()
|
||||||
|
window.clearInterval(updateTaskId)
|
||||||
|
playing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var midiIputs
|
||||||
|
midiSelect.onfocus = function() {
|
||||||
|
midiIputs = {}
|
||||||
|
midiSelect.innerHTML = `<option value="">Aucun</option>`
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
midiSelect.oninput = () => {
|
||||||
|
for (const id in midiIputs) midiIputs[id].onmidimessage = null
|
||||||
|
if (midiSelect.value) {
|
||||||
|
midiIputs[midiSelect.value].onmidimessage = onMIDIMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMIDIMessage(event) {
|
||||||
|
let [code, note, velocity] = event.data
|
||||||
|
|
||||||
|
if (144 <= code && code <= 159 && canonSprites[note]) {
|
||||||
|
//playNote(note, velocity / 128)
|
||||||
|
canonSprites[note].shooting = true
|
||||||
|
} else if (128 <= code && code <= 143 && canonSprites[note]) {
|
||||||
|
//stopNote(note)
|
||||||
|
canonSprites[note].shooting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsDialog.onclose = newGame
|
||||||
|
|
||||||
|
let level
|
||||||
|
function newGame() {
|
||||||
|
settingsDialog.onclose = resume
|
||||||
|
level = 0
|
||||||
|
nextLevel()
|
||||||
|
}
|
||||||
|
|
||||||
|
let midiSong
|
||||||
|
let noteSprites = []
|
||||||
|
let explosionSprites = []
|
||||||
|
async function nextLevel() {
|
||||||
|
level++
|
||||||
|
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.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)
|
||||||
|
Tone.Transport.scheduleOnce(time => noteSprites.push(noteSprite), note.time)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Tone.Transport.scheduleOnce(time => nextLevel, midiSong.duration)
|
||||||
|
|
||||||
|
levelDialog.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
levelDialog.onclose = resume
|
||||||
|
|
||||||
|
let updateTaskId
|
||||||
|
function resume() {
|
||||||
|
playing = true
|
||||||
|
Tone.Transport.start()
|
||||||
|
updateTaskId = window.setInterval(update, UPDATE_PERIOD)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
noteSprites.forEach(noteSprite => {
|
||||||
|
noteSprite.y += 0.5
|
||||||
|
})
|
||||||
|
noteSprites.filter(noteSprite => noteSprite.y >= 420).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)
|
||||||
|
|
||||||
|
canonSprites.forEach(canonSprite => {
|
||||||
|
let noteSprite = noteSprites.find(noteSprite => noteSprite.note == canonSprite.note)
|
||||||
|
if (noteSprite) {
|
||||||
|
noteSprite.shot = canonSprite.shooting
|
||||||
|
if (noteSprite.shot) {
|
||||||
|
noteSprite.duration -= UPDATE_PERIOD
|
||||||
|
if (noteSprite.duration > 0) {
|
||||||
|
playNote(canonSprite.note)
|
||||||
|
canonSprite.impactY = noteSprite.y
|
||||||
|
} else {
|
||||||
|
stopNote(canonSprite.note)
|
||||||
|
let explosionSprite = noteSprite.explose()
|
||||||
|
explosionSprites.push(explosionSprite)
|
||||||
|
explosionSprite.play().then(() => explosionSprites.remove(explosionSprite))
|
||||||
|
noteSprites.remove(noteSprite)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopNote(canonSprite.note)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopNote(canonSprite.note)
|
||||||
|
canonSprite.impactY = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNote(note, velocity=0.7) {
|
||||||
|
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, audioCtx.currentTime + 0.05)
|
||||||
|
oscillator.connect(oscillator.velocity)
|
||||||
|
oscillator.start()
|
||||||
|
oscillator.velocity.connect(volume)
|
||||||
|
|
||||||
|
depth.connect(oscillator.detune)
|
||||||
|
|
||||||
|
oscillators[note] = oscillator
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopNote(note) {
|
||||||
|
if(!oscillators[note]) return
|
||||||
|
|
||||||
|
velocity = oscillators[note].velocity.gain.value
|
||||||
|
oscillators[note].velocity.gain.setValueCurveAtTime([velocity, velocity/10, velocity/20, 0], audioCtx.currentTime + 0.1, 0.5)
|
||||||
|
delete(oscillators[note])
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNoise(noiseDuration, startGain=0.5, 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([startGain, startGain/5, 0], audioCtx.currentTime, noiseDuration)
|
||||||
|
noise.connect(bandpass).connect(gain).connect(audioCtx.destination)
|
||||||
|
noise.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onkeydown = function(event) {
|
||||||
|
if (playing && keyMap.includes(event.key)) {
|
||||||
|
event.preventDefault()
|
||||||
|
let note = FIRST_NOTE + keyMap.indexOf(event.key)
|
||||||
|
canonSprites[note].shooting = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onkeyup = function(event) {
|
||||||
|
if (playing && keyMap.includes(event.key)) {
|
||||||
|
event.preventDefault()
|
||||||
|
let note = FIRST_NOTE + keyMap.indexOf(event.key)
|
||||||
|
canonSprites[note].shooting = false
|
||||||
|
}
|
||||||
|
}
|
BIN
img/big-explosion.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
img/canon.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/console.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
img/favicon.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/little-explosion.png
Normal file
After Width: | Height: | Size: 832 B |
BIN
img/note.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
img/notes.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
img/pixel-city-chill.gif
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
img/synthe.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
img/tiny-explosion.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
img/tiny-notes.png
Normal file
After Width: | Height: | Size: 15 KiB |
93
index.html
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Angry notes from outer space</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="">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.css" rel="stylesheet">
|
||||||
|
<link href="style.css" rel="stylesheet">
|
||||||
|
<script src="http://unpkg.com/tone"></script>
|
||||||
|
<script src="https://unpkg.com/@tonejs/midi"></script>
|
||||||
|
</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>
|
||||||
|
</section>
|
||||||
|
<dialog id="startDialog" class="nes-dialog is-rounded is-dark">
|
||||||
|
<form method="dialog">
|
||||||
|
<h1 class="title is-centered">Angry notes from outer space</h1>
|
||||||
|
<div style="display: flex;gap: 2rem;align-items: end;">
|
||||||
|
<div>
|
||||||
|
<p>Jean-Michel, les notes furieuses de l'espace attaquent ! Détruisez-les à l'aide de votre harpe laser.</p>
|
||||||
|
<p>Ce jeu est conçu pour un clavier MIDI. Si vous en avez un, branchez-le maintenant.</p>
|
||||||
|
</div>
|
||||||
|
<img src="img/notes.png">
|
||||||
|
</div>
|
||||||
|
<div class="is-centered">
|
||||||
|
<button class="nes-btn is-primary">Continuer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<dialog id="settingsDialog" class="nes-dialog is-rounded is-dark">
|
||||||
|
<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 for="keyMapInput" style="padding: 0 2em 0 1em; margin: 0 6px;">C C♯ D D♯ E F F♯ G G♯ A A♯ B C C♯ D D♯ E F F♯ G G♯ A A♯ B C</label>
|
||||||
|
<textarea id="keyMapInput" class="nes-textarea is-dark" minlength="25" maxlength="25" cols="25" required
|
||||||
|
style="text-transform: uppercase; letter-spacing: 1.6em;"
|
||||||
|
placeholder="wsxdcvgbhnj,e'r(tyèu_içop" autocomplete="off" autocorrect="off" spellcheck="false">wsxdcvgbhnj,e'r(tyèu_içop</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label for="midiSelect" style="color:#fff">Clavier MIDI</label>
|
||||||
|
<div class="nes-select is-dark">
|
||||||
|
<select id="midiSelect">
|
||||||
|
<option value="">Aucun</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="nes-container with-title is-dark">
|
||||||
|
<h3 class="title">Son</h3>
|
||||||
|
<div class="nes-field sliders">
|
||||||
|
<label><input type="range" id="volRange" min="0" max="1" step="any" value="0.400" orient="vertical" onmousedown="playNote(57)" onmouseup="stopNote(57)">Vol</label>
|
||||||
|
<label><input type="range" id="modRange" min="0" max="100" step="any" value="45.00" orient="vertical" onmousedown="playNote(57)" onmouseup="stopNote(57)">Mod</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.200" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">1f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">2f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.200" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">3f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">4f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">5f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">6f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">7f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">8f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">9f</label>
|
||||||
|
<label><input type="range" class="partial" min="0" max="0.2" step="any" value="0.000" orient="vertical" oninput="onpartialinput()" onmousedown="playNote(57)" onmouseup="stopNote(57)">10f</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<menu class="is-centered">
|
||||||
|
<button id="playButton" class="nes-btn is-primary" tabindex="1">OK</button>
|
||||||
|
</menu>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<dialog id="levelDialog" class="nes-dialog is-rounded is-dark">
|
||||||
|
<form method="dialog">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<button id="settingsButton" type="button" class="nes-btn">
|
||||||
|
<span>
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm16 5H4v2h16v-2z" fill="currentColor"></path></svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
midi/1.mid
Normal file
BIN
midi/2.mid
Normal file
157
style.css
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
html, body, pre, code, kbd, samp {
|
||||||
|
font-family: "Press Start 2P", monospace;;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #212529;
|
||||||
|
font-size: 12;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before,
|
||||||
|
.nes-dialog::before {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-image: repeating-linear-gradient(#0004, #0004 1px, transparent 1px, transparent 2px);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 2;
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nes-dialog {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 0 4px #adafbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliders {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliders label {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
margin: 5px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range][orient=vertical] {
|
||||||
|
-webkit-appearance: slider-vertical;
|
||||||
|
width: 4px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range] {
|
||||||
|
padding: 0;
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 3px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 3px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-ms-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 3px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
background: white;
|
||||||
|
border: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-track {
|
||||||
|
background: transparent;
|
||||||
|
width: 3px;
|
||||||
|
border-left: 0;
|
||||||
|
border-top: 3px solid white;
|
||||||
|
border-bottom: 3px solid white;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: -0 7px;
|
||||||
|
background-image: linear-gradient(white);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-runnable-track {
|
||||||
|
background: transparent;
|
||||||
|
width: 3px;
|
||||||
|
border-left: 0;
|
||||||
|
border-top: 3px solid white;
|
||||||
|
border-bottom: 3px solid white;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: -0 7px;
|
||||||
|
background-image: linear-gradient(white);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-ms-track {
|
||||||
|
background: transparent;
|
||||||
|
width: 3px;
|
||||||
|
border-left: 0;
|
||||||
|
border-top: 3px solid white;
|
||||||
|
border-bottom: 3px solid white;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: -0 7px;
|
||||||
|
background-image: linear-gradient(white);
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsButton {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 25px;
|
||||||
|
right: 2rem;
|
||||||
|
box-shadow: 0 5px 20px rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 32px;
|
||||||
|
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 {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
border-image-outset: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nes-btn {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nes-btn:active:not(.is-disabled)::after {
|
||||||
|
box-shadow: inset 2px 2px #adafbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nes-btn:hover::after {
|
||||||
|
box-shadow: inset -2px -2px #adafbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nes-btn::after {
|
||||||
|
box-shadow: inset -2px -2px #adafbc;
|
||||||
|
}
|
50
test.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<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>
|