← back to Neon Baguette Battle
Play La Marseillaise on load + cobblestone street floor strewn with croissants
125754fbfeeee72c2b100f729eb7867a72bcb92a · 2026-08-31 13:51:10 -0700 · Steve Abrams
- Anthem: opening of La Marseillaise synthesized on the WebAudio melody synth; plays on load (starts on first gesture per autoplay policy), M mutes, stops on mute
- Floor: neon-grid replaced with a perspective cobblestone street (neon-rimmed stones) + scattered street croissants
- Harden roundRectPath against negative radius (thin far cobble rows)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 125754fbfeeee72c2b100f729eb7867a72bcb92a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 13:51:10 2026 -0700
Play La Marseillaise on load + cobblestone street floor strewn with croissants
- Anthem: opening of La Marseillaise synthesized on the WebAudio melody synth; plays on load (starts on first gesture per autoplay policy), M mutes, stops on mute
- Floor: neon-grid replaced with a perspective cobblestone street (neon-rimmed stones) + scattered street croissants
- Harden roundRectPath against negative radius (thin far cobble rows)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
README.md | 6 +++-
index.html | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 92 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index a6da8e9..eba191e 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,11 @@ full, otherwise it does a whack. (In 2-player the arrows belong to Player 2.)
- **Flying food hazards**: pies (cream splat, big damage), french rolls (fast),
and croissants arc in from off-screen and hit either fighter.
- **Neon Paris arena**: animated Eiffel Tower, moon, twinkling stars, skyline
- with lit windows, glowing perspective-grid floor, screen shake + vignette.
+ with lit windows, a **cobblestone street** floor (perspective stones with
+ neon rims) strewn with croissants, screen shake + vignette.
+- **La Marseillaise** (the French anthem) plays on load, synthesized on the
+ WebAudio synth. Browsers gate autoplay, so it starts on your first click or
+ keypress; `M` mutes it (and all audio).
- **Sound**: WebAudio synth SFX for pokes/swings/blocks/blasts/KO, plus comedic
**voice lines** via the browser Speech API — an American hamming up a French
accent ("Take zat!", "Oh la la, magnifique!", "Zut alors!").
diff --git a/index.html b/index.html
index e7c2b4f..b2ba00e 100644
--- a/index.html
+++ b/index.html
@@ -202,6 +202,40 @@
const f = a.createBiquadFilter(); f.type='bandpass'; f.frequency.value=1400;
n.connect(f); f.connect(g); g.connect(a.destination); n.start();
}
+ // ---- La Marseillaise (opening) played on the melody synth ----
+ let anthemStarted=false, anthemOsc=[];
+ const NF = { 'D4':293.66, 'F#4':369.99, 'G4':392.00, 'A4':440.00, 'B4':493.88, 'C5':523.25, 'D5':587.33 };
+ const ANTHEM = [ // [note, beats] — "Allons enfants de la patrie, le jour de gloire est arrivé..."
+ ['D4',.5],['D4',.75],['D4',.25],['G4',1],['G4',.5],['A4',.5],['A4',1],
+ ['D5',.75],['B4',.25],['G4',.5],['G4',.5],['B4',.75],['G4',.25],['C5',1],['A4',1],
+ ['F#4',.5],['G4',.5],['G4',.5],['A4',.5],['B4',.75],['B4',.25],['B4',.5],['C5',1],
+ ['B4',.5],['B4',.5],['A4',.5],['A4',.5],['B4',.75],['C5',.25],['C5',.5],['C5',.5],['D5',1],['C5',.5],['B4',1.5]
+ ];
+ function _scheduleAnthem(a){
+ const beat=0.42; let t=a.currentTime+0.12;
+ const master=a.createGain(); master.gain.value=0.9; master.connect(a.destination);
+ for (const [n,b] of ANTHEM){
+ const f=NF[n], dur=b*beat;
+ const o=a.createOscillator(), g=a.createGain(), lp=a.createBiquadFilter();
+ o.type='sawtooth'; o.frequency.value=f;
+ lp.type='lowpass'; lp.frequency.value=2200;
+ g.gain.setValueAtTime(0.0001, t);
+ g.gain.exponentialRampToValueAtTime(0.13, t+0.03);
+ g.gain.setValueAtTime(0.13, t+dur*0.72);
+ g.gain.exponentialRampToValueAtTime(0.0001, t+dur*0.99);
+ o.connect(lp); lp.connect(g); g.connect(master);
+ o.start(t); o.stop(t+dur+0.02);
+ anthemOsc.push(o);
+ t+=dur;
+ }
+ }
+ function startAnthem(){
+ if (anthemStarted || muted) return;
+ const a=ensure(); if(!a) return;
+ const go=()=>{ if(anthemStarted) return; anthemStarted=true; _scheduleAnthem(a); };
+ if (a.state==='suspended') a.resume().then(go).catch(()=>{}); else go();
+ }
+ function stopAnthem(){ for(const o of anthemOsc){ try{o.stop();}catch(e){} } anthemOsc=[]; }
return {
poke: () => tone(520, .08, 'square', .12, 380),
swing: () => { tone(180,.18,'sawtooth',.16,90); noise(.14,.14); },
@@ -211,8 +245,9 @@
jump: () => tone(300,.12,'sine',.1,540),
ko: () => { tone(120,.5,'sawtooth',.2,50); noise(.4,.2); },
bell: () => { tone(880,.5,'sine',.12); tone(1320,.5,'sine',.06); },
- toggle: () => { muted = !muted; return muted; },
+ toggle: () => { muted = !muted; if (muted) stopAnthem(); return muted; },
resume: () => { const a = ensure(); if (a && a.state === 'suspended') a.resume(); },
+ startAnthem, stopAnthem,
get muted(){ return muted; }
};
})();
@@ -664,7 +699,7 @@
}
function roundRectPath(g,x,y,w,h,r){
- r = Math.min(r, w/2, h/2);
+ r = Math.max(0, Math.min(r, w/2, h/2)); // never negative (thin cobble rows)
g.beginPath();
g.moveTo(x+r,y);
g.arcTo(x+w,y,x+w,y+h,r);
@@ -877,23 +912,53 @@
g.beginPath(); g.arc(cx, topY-26, 3+Math.sin(bgT*6)*1.5, 0, Math.PI*2); g.fill();
g.restore();
}
- function drawFloor(g){
- g.fillStyle = '#0b0714'; g.fillRect(0, GROUND, W, H-GROUND);
- g.save();
- g.strokeStyle='rgba(255,61,154,.6)'; g.shadowColor='#ff3d9a'; g.shadowBlur=8; g.lineWidth=2;
- g.beginPath(); g.moveTo(0,GROUND); g.lineTo(W,GROUND); g.stroke();
- // perspective grid
- g.strokeStyle='rgba(34,224,255,.35)'; g.shadowColor='#22e0ff'; g.lineWidth=1;
- const vpx = W/2;
- for (let i=-10;i<=10;i++){
- const gx = vpx + i*90;
- g.beginPath(); g.moveTo(vpx + i*16, GROUND); g.lineTo(gx, H); g.stroke();
+ const CROISSANT_SPOTS = [
+ [150, GROUND+34, 1.00, 0.35], [395, GROUND+16, 0.75, -0.25], [590, GROUND+64, 1.35, 0.50],
+ [815, GROUND+28, 0.85, -0.50], [300, GROUND+92, 1.55, 0.15], [700, GROUND+96, 1.6, -0.3]
+ ];
+ function drawStreetCroissants(g){
+ for (const [x,y,s,rot] of CROISSANT_SPOTS){
+ g.save(); g.translate(x,y);
+ g.fillStyle='rgba(0,0,0,.35)'; g.beginPath(); g.ellipse(0, 4*s, 15*s, 5*s, 0, 0, Math.PI*2); g.fill(); // shadow
+ g.rotate(rot); g.scale(s,s);
+ g.fillStyle='#e0a94e'; g.strokeStyle='#a9761f'; g.lineWidth=2;
+ g.beginPath(); g.arc(0,0,13,0.18*Math.PI,1.82*Math.PI); g.arc(5,0,8,1.82*Math.PI,0.18*Math.PI,true); g.closePath(); g.fill(); g.stroke();
+ g.strokeStyle='rgba(120,72,20,.5)'; g.lineWidth=1;
+ for (let i=-1;i<=1;i++){ g.beginPath(); g.moveTo(i*5,-6); g.lineTo(i*5,6); g.stroke(); }
+ g.fillStyle='rgba(255,245,220,.3)'; g.beginPath(); g.ellipse(-3,-4,4,2,0.4,0,Math.PI*2); g.fill(); // flour sheen
+ g.restore();
}
- for (let j=1;j<=6;j++){
- const y = GROUND + (H-GROUND)*(j/6)*(j/6);
- g.beginPath(); g.moveTo(0,y); g.lineTo(W,y); g.stroke();
+ }
+ function drawFloor(g){
+ // stone base
+ const fg = g.createLinearGradient(0,GROUND,0,H);
+ fg.addColorStop(0,'#2a2033'); fg.addColorStop(1,'#0b0712');
+ g.fillStyle=fg; g.fillRect(0, GROUND, W, H-GROUND);
+
+ // cobblestones, in perspective (rows grow toward the viewer)
+ const rows=7, span=H-GROUND;
+ for (let j=0;j<rows;j++){
+ const t0=j/rows, t1=(j+1)/rows;
+ const y0=GROUND + span*t0*t0;
+ const y1=GROUND + span*t1*t1;
+ const rh=y1-y0, sw=26 + t0*80, off=(j%2)*(sw*0.5);
+ const pad=Math.min(2, rh*0.2), sh=Math.max(1, rh-pad*2), rr=Math.max(0, Math.min(10, rh*0.3));
+ for (let x=-sw; x<W+sw; x+=sw){
+ const cx=x+off;
+ const h=(Math.sin(cx*0.21 + j*1.7)*0.5+0.5); // stable per-stone shade
+ const r=Math.round(40+h*20), gg=Math.round(30+h*14), bl=Math.round(50+h*22);
+ roundRectPath(g, cx+2, y0+pad, sw-4, sh, rr);
+ g.fillStyle='rgb('+r+','+gg+','+bl+')'; g.fill();
+ g.lineWidth=1.4; g.strokeStyle='rgba(255,61,154,'+(0.05+t0*0.13).toFixed(3)+')'; g.stroke(); // neon rim
+ g.strokeStyle='rgba(185,215,255,'+(0.04+t0*0.09).toFixed(3)+')'; g.lineWidth=1;
+ g.beginPath(); g.moveTo(cx+5, y0+3); g.lineTo(cx+sw-6, y0+3); g.stroke(); // top highlight
+ }
}
- g.restore();
+ // glowing kerb at the horizon
+ g.save(); g.strokeStyle='rgba(255,61,154,.7)'; g.shadowColor='#ff3d9a'; g.shadowBlur=10; g.lineWidth=2;
+ g.beginPath(); g.moveTo(0,GROUND); g.lineTo(W,GROUND); g.stroke(); g.restore();
+
+ drawStreetCroissants(g);
}
// ---------- HUD ----------
@@ -1233,6 +1298,11 @@
}
buildRoster();
+ // La Marseillaise on load — try immediately; browsers gate autoplay, so also fire on the first interaction
+ Audio.startAnthem();
+ const kickAnthem = () => Audio.startAnthem();
+ window.addEventListener('pointerdown', kickAnthem, { once:true });
+ window.addEventListener('keydown', kickAnthem, { once:true });
requestAnimationFrame(loop);
})();
</script>
← 4fcd312 Bigger, faster baguette missile + alternate 1P controls (arr
·
back to Neon Baguette Battle
·
Broaden P1 controls: arrows move; Space/Return/Cmd/Option/Ct a39d05a →