[object Object]

← 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 →