Gap Flight — Coordinate airborne impulses, obstacle gaps and one-time crossing scores with collision-first termination.

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Gap Flight</title><style>:root{--bg:#eef9ff;--ink:#173a59;--accent:#266eb0;--line:#b9d5e8;--wash:#dfedf9;}*{box-sizing:border-box}body{margin:0;font:16px/1.5 Arial,Helvetica,sans-serif;color:var(--ink);background:var(--bg)}main{max-width:1120px;padding:24px;margin:auto}h1,h2,h3,p{margin:0}h1{font-size:clamp(26px,4vw,38px);line-height:1.15;letter-spacing:-.03em}h2{font-size:23px}h3{font-size:18px}header{margin-bottom:20px}header p{margin-top:8px}.eyebrow{text-transform:uppercase;letter-spacing:.13em;font-size:12px;font-weight:bold;color:var(--accent)}button,input,select,textarea{font:inherit}button{cursor:pointer;min-height:44px;padding:9px 15px;border:1px solid var(--accent);background:var(--accent);color:white;border-radius:7px;font-weight:600}button.secondary{background:white;color:var(--accent)}button:disabled{opacity:.45;cursor:default}input,select,textarea{width:100%;min-height:44px;border:1px solid #a2aaba;border-radius:5px;background:white;color:var(--ink);padding:9px}textarea{resize:vertical;min-height:95px}label{display:block;font-size:14px;font-weight:bold;margin:12px 0 5px}input[type=checkbox]{width:20px;min-height:20px;vertical-align:middle;margin-right:8px}:focus-visible{outline:3px solid #df7a16;outline-offset:3px}.actions{display:flex;gap:9px;flex-wrap:wrap;margin:14px 0}.panel{padding:22px;background:white;border:1px solid var(--line);border-radius:12px;min-width:0}.columns{display:grid;grid-template-columns:1.6fr 1fr;gap:20px}.two{display:grid;grid-template-columns:1fr 1fr;gap:18px}.stats{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:15px}.stat{flex:1;min-width:92px;background:var(--wash);padding:10px 14px;border-radius:8px}.stat small{display:block}.stat strong{font-size:25px}.muted{color:#526178;font-size:14px}.notice{padding:14px 16px;background:var(--wash);border-left:4px solid var(--accent);margin:14px 0;overflow-wrap:anywhere}.error{color:#a12e25;font-weight:bold}.below{margin-top:28px;padding-top:22px;border-top:1px solid var(--line)}.below p{margin-top:9px}svg.board{width:100%;display:block;border-radius:10px;border:1px solid var(--line)}table{border-collapse:collapse;width:100%;font-size:14px}td,th{text-align:left;padding:10px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}th{font-size:12px;text-transform:uppercase;letter-spacing:.04em}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:14px/1.6 ui-monospace,monospace}.scroll{overflow-x:auto}.tag{font-size:12px;padding:4px 8px;background:var(--wash);border-radius:4px}.big{font-size:clamp(36px,6vw,62px);font-weight:bold;line-height:1.15}.foot{font-size:14px;color:#526178;margin-top:25px}details{margin:18px 0}summary{cursor:pointer;font-weight:bold;min-height:44px;padding:10px 0}[hidden]{display:none!important}@media(max-width:800px){.columns{grid-template-columns:1fr}.two{grid-template-columns:1fr}}@media(max-width:480px){main{padding:18px 14px}.panel{padding:16px}.stat{min-width:78px;padding:8px}.actions button{flex-grow:1}td,th{padding:7px}.big{font-size:38px}}#game-region{border-radius:14px}.flight-board{background:#91d8fa;padding:12px;border-radius:14px}.flight-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.score-disc{background:#ffe88e;color:#63491a;border-radius:50%;width:80px;height:80px;display:grid;place-content:center;text-align:center}.score-disc strong{font-size:30px;line-height:1}.lab{background:#fff9e7;border:1px solid #e6ce88;border-radius:12px;padding:20px}.lab .stat{background:#fff0bd}#pair-list{min-height:60px}.game-controls button{min-width:90px}</style></head><body><main><header><p class="eyebrow">Original rectangle flyer</p><h1>Gap Flight</h1><p>Tap Flap to set upward speed; fit through each opening to earn one point per pair.</p></header><div class="columns"><section id="game-region" tabindex="0" aria-label="Gap Flight controls. Space or Arrow Up flaps when this region is focused."><div class="flight-title"><div><h2 id="state">Ready</h2><p id="telemetry" class="muted">y 112.00 · velocity 0.00 · 0.00 s</p></div><div class="score-disc"><small>Score</small><strong id="score">0</strong></div></div><div class="flight-board"><svg id="field" class="board" viewBox="0 0 320 240" role="img" aria-label="Flyer and moving pairs with an opening from y 80 to 160"></svg></div><div class="actions game-controls"><button type="button" id="start">Start</button><button type="button" id="flap">Flap ↑</button><button type="button" id="pause" class="secondary">Pause</button><button type="button" id="resume" class="secondary">Resume</button><button type="button" id="reset" class="secondary">Reset</button></div><p id="message" class="notice" role="status">Start does not flap. Use fresh taps, Space or Arrow Up inside the game region.</p><p class="muted">Gap y 80–160 · flyer 16 × 16 · one score per obstacle pair</p></section><aside class="panel"><h2>Flight instruments</h2><p class="notice" id="pair-list">No active pairs</p><p>Gravity: 400 px/s²<br>Fresh flap: −180 px/s<br>Pair speed: 70 px/s<br>New pair: every 2.4 s</p><h3 style="margin-top:18px">Pass rule</h3><p>Collision is checked first. A pair scores only after its right edge is strictly left of x 64. Its two blocks share one ID.</p><p class="muted" style="margin-top:12px">Focus may move among these game buttons. Leaving the game region or hiding the page pauses the run.</p></aside></div>
<section class="below"><h2>Motion lab · isolated calculation</h2><p>Inspect the authored impulse fixture independently of a live run. Lab steps do not award game points.</p><div class="lab"><div class="stats"><div class="stat"><small>Lab position y</small><strong id="lab-y">112.00</strong></div><div class="stat"><small>Lab velocity</small><strong id="lab-v">0.00</strong></div><div class="stat"><small>Lab time</small><strong id="lab-time">0.00 s</strong></div></div><div class="actions"><button type="button" id="lab-flap">Apply lab flap</button><button type="button" id="lab-step" class="secondary">Advance lab 0.10 s</button><button type="button" id="lab-reset" class="secondary">Reset lab</button></div><p id="lab-result" role="status">Initial fixture: y = 112, velocity = 0.</p></div><p class="foot">Original fixed-gap learning prototype; no commercial assets, audio, storage or leaderboard. Each live update is 0.01 s with subdivided collision checks. At most ten updates run per animation frame; extra time is discarded as slowdown. Hidden/paused intervals do not catch up. Fixed gaps are not a guarantee of fairness or identical frame-rate performance. Browser and host inspection remain pending.</p></section></main><script>'use strict';const $=id=>document.getElementById(id);let s,gen=0,frameId=0,last=null,acc=0,held=new Set(),lab={y:112,v:0,tick:0};
function fresh(){return {state:'Ready',tick:0,y:112,v:0,score:0,next:0,index:0,pairs:[]};}
function flap(){if(s.state==='Running')s.v=-180;render();}
function hit(y,x){return y<0||y+16>240||(x<80&&x+36>64&&(y<80||y+16>160));}
function advance(){if(s.state!=='Running')return;const oldY=s.y,oldV=s.v,oldTick=s.tick;s.tick++;for(const p of s.pairs)p.x=320-70*(s.tick-p.born)/100;const newY=oldY+oldV*.01+200*.01*.01;s.v+=4;let collision=false;for(let j=0;j<=10;j++){const dt=.01*j/10;const y=oldY+oldV*dt+200*dt*dt;const boundary=y<0||y+16>240;const block=s.pairs.some(p=>!p.passed&&hit(y,320-70*((oldTick-p.born)/100+dt)));if(boundary||block){collision=true;break;}}s.y=newY;if(collision){s.state='Lost';held.clear();return;}for(const p of s.pairs){if(!p.passed&&p.x+36<64){p.passed=true;s.score++;}}s.pairs=s.pairs.filter(p=>!p.passed);if(s.tick>=s.next){spawn();}}
function spawn(){s.pairs.push({id:s.generation+'/P'+(++s.index),born:s.tick,x:320,passed:false});s.next=s.tick+240;}
function cancel(){gen++;cancelAnimationFrame(frameId);last=null;acc=0;held.clear();}
function loop(stamp,g){if(g!==gen||s.state!=='Running')return;if(last!==null){acc+=Math.max(0,(stamp-last)/1000);const n=Math.min(10,Math.floor((acc+1e-10)/.01));for(let i=0;i<n;i++)advance();acc=acc>=.1?0:Math.max(0,acc-n*.01);}last=stamp;render();if(s.state==='Running')frameId=requestAnimationFrame(t=>loop(t,g));}
function schedule(){cancel();const g=gen;frameId=requestAnimationFrame(t=>loop(t,g));}
function start(animated=true){if(s.state!=='Ready')return;s.state='Running';s.generation=gen;spawn();if(animated)schedule();render();}
function pause(){if(s.state==='Running'){s.state='Paused';cancel();render();}}
function reset(){cancel();s=fresh();$('message').textContent='Start does not flap. Use fresh taps, Space or Arrow Up inside the game region.';render();}
function render(){$('state').textContent=s.state;$('score').textContent=s.score;$('telemetry').textContent='y '+s.y.toFixed(2)+' · velocity '+s.v.toFixed(2)+' · '+(s.tick/100).toFixed(2)+' s';$('pair-list').textContent=s.pairs.map(p=>p.id+' · x '+p.x.toFixed(1)+' · active').join(' | ')||'No active pairs';if(s.state==='Lost')$('message').textContent='Lost — collision. No point is awarded on a collision update. Reset for a new run.';else if(s.state==='Paused')$('message').textContent='Paused — time and held input are cleared. Resume when ready.';else if(s.state==='Running')$('message').textContent='Running — fresh flaps set velocity to −180.';$('start').disabled=s.state!=='Ready';$('flap').disabled=s.state!=='Running';$('pause').disabled=s.state!=='Running';$('resume').disabled=s.state!=='Paused';$('field').innerHTML='<rect width="320" height="240" fill="#a9e5ff"/><path d="M0 80H320M0 160H320" stroke="#fff" stroke-dasharray="5 5"/><text x="10" y="76" font-size="10" fill="#265c80">80 · GAP TOP</text><text x="10" y="173" font-size="10" fill="#265c80">160 · GAP BOTTOM</text>'+s.pairs.map(p=>'<g fill="#496cba"><rect x="'+p.x+'" width="36" height="80" rx="3"/><rect x="'+p.x+'" y="160" width="36" height="80" rx="3"/></g>').join('')+'<rect x="64" y="'+s.y+'" width="16" height="16" rx="3" fill="#ffbe4c" stroke="#94520e" stroke-width="2"/><path d="M68 '+(s.y+8)+'h8" stroke="white" stroke-width="2"/>';}
function labRender(){$('lab-y').textContent=lab.y.toFixed(2);$('lab-v').textContent=lab.v.toFixed(2);$('lab-time').textContent=(lab.tick/100).toFixed(2)+' s';$('lab-result').textContent='Lab result: y = '+lab.y.toFixed(2)+', velocity = '+lab.v.toFixed(2)+'.';}
$('start').addEventListener('click',()=>start());$('flap').addEventListener('click',flap);$('flap').addEventListener('keydown',e=>{if(e.repeat&&(e.key==='Enter'||e.key===' '))e.preventDefault();});$('pause').addEventListener('click',pause);$('resume').addEventListener('click',()=>{if(s.state==='Paused'){s.state='Running';schedule();render();}});$('reset').addEventListener('click',reset);
$('game-region').addEventListener('keydown',e=>{if((e.key===' '||e.key==='ArrowUp')&&!e.isComposing&&e.keyCode!==229){if(e.target.tagName==='BUTTON'&&e.key===' ')return;e.preventDefault();if(!e.repeat&&!held.has(e.key)){held.add(e.key);flap();}}});$('game-region').addEventListener('keyup',e=>held.delete(e.key));$('game-region').addEventListener('focusout',e=>{if(!$('game-region').contains(e.relatedTarget))pause();});document.addEventListener('visibilitychange',()=>{if(document.hidden)pause();});
$('lab-flap').addEventListener('click',()=>{lab.v=-180;labRender();});$('lab-step').addEventListener('click',()=>{for(let i=0;i<10;i++){lab.y+=lab.v*.01+.02;lab.v+=4;lab.tick++;}labRender();});$('lab-reset').addEventListener('click',()=>{lab={y:112,v:0,tick:0};labRender();});globalThis.app={get:()=>s,start,advance,flap,pause,reset,hit,loop,lab:()=>lab};reset();</script></body></html>