SOURCE / PINNED RELEASE
Made of little things.
quacky duck
- Release
- dbf7d6c6b06a…
- Author-recorded commit
- Not provided
- License
- Check the original source
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Napplet</title>
<script type="module" crossorigin>function H(e,n){return!!(e!=null&&e[n])}function U(e){return H(window.napplet,e)}function W(){const e=new Map;function n(r,x){if(e.has(r))throw new Error(`NAP domain "${r}" is already registered`);e.set(r,x)}function a(r){const x=r.type.indexOf(".");if(x<=0)return!1;const G=r.type.slice(0,x),F=e.get(G);return F?(F(r),!0):!1}function C(){return Array.from(e.keys())}return{registerNap:n,dispatch:a,getRegisteredDomains:C}}var X=W(),Y=X.registerNap,J="theme";function O(){var n;const e=window;if(!((n=e.napplet)!=null&&n.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function K(){return O().get()}function Q(e){return O().onChanged(e)}Y(J,e=>{});const V=document.querySelector("#app"),P=document.createElement("div");P.className="toolbar";const T=document.createElement("span");T.className="score";const k=document.createElement("span");k.className="best";const R=document.createElement("button");R.textContent="Flap";const g=document.createElement("button");g.textContent="↻";g.title="Restart";g.setAttribute("aria-label","Restart game");P.append(T,k,R,g);const y=document.createElement("div");y.className="field";const i=document.createElement("canvas");i.tabIndex=0;i.setAttribute("aria-label","Duck flying game. Tap, click, or press Space to flap. Avoid the reeds.");const S=document.createElement("div");S.className="message";const $=document.createElement("strong"),z=document.createElement("span");S.append($,z);y.append(i,S);V.replaceChildren(P,y);const t=i.getContext("2d");let l=400;const w=480;let N=1,o="ready",s=220,u=0,p=0,h=0,b=0,v=0,c=[];const m=()=>Math.min(100,l*.26),f=448,E=154;function M(){T.textContent=`◈ ${p}`,k.textContent=`Best ${h}`,S.hidden=o==="playing",$.textContent=o==="over"?`Splash! Score ${p}`:"Ready to fly?",z.textContent=o==="over"?"Tap or flap to try again":"Tap or press Space to flap",R.textContent=o==="over"?"Retry":"Flap"}function A(){o="ready",s=220,u=0,p=0,c=[],M()}function D(){o==="over"&&A(),o==="ready"&&(o="playing",c.push({x:m()+330,gap:220,passed:!1}),M()),u=-260}R.addEventListener("click",D);g.addEventListener("click",A);i.addEventListener("pointerdown",e=>{e.preventDefault(),i.focus(),D()});i.addEventListener("keydown",e=>{(e.code==="Space"||e.code==="ArrowUp")&&(e.preventDefault(),e.repeat||D())});function B(){const e=y.getBoundingClientRect();if(!e.width||!e.height)return;l=e.width/e.height*w;const n=Math.min(devicePixelRatio||1,2);i.width=Math.round(e.width*n),i.height=Math.round(e.height*n),N=i.height/w}const _=new ResizeObserver(B);_.observe(y);function d(e,n,a,C,r){t.fillStyle=r,t.beginPath(),t.ellipse(e,n,a,C,0,0,Math.PI*2),t.fill()}function I(){o="over",h=Math.max(h,p),M()}function Z(e){if(b+=e,o!=="playing")return;u+=760*e,s+=u*e;for(const a of c)a.x-=136*e,!a.passed&&a.x+52<m()-12&&(a.passed=!0,p++,h=Math.max(h,p),M()),m()+16>a.x&&m()-16<a.x+52&&(s-13<a.gap-E/2||s+13>a.gap+E/2)&&I();const n=c[c.length-1];n&&n.x<l+20&&c.push({x:n.x+235,gap:145+Math.random()*150,passed:!1}),c=c.filter(a=>a.x>-70),(s<13||s>f-13)&&I()}function ee(){t.setTransform(N,0,0,N,0,0),t.fillStyle="#d5eee9",t.fillRect(0,0,l,w),t.fillStyle="#f6f6d8",t.beginPath(),t.arc(l-65,72,27,0,Math.PI*2),t.fill();for(let e=-100;e<l+150;e+=190)d(e+70,365,140,76,"#b4d6bd"),d(e+145,413,120,65,"#91bda8");for(const e of c){const n=e.gap-E/2,a=e.gap+E/2;t.fillStyle="#386e53",t.fillRect(e.x,0,52,n),t.fillRect(e.x,a,52,f-a),t.fillStyle="#5e9470",t.fillRect(e.x+6,0,10,n-5),t.fillRect(e.x+6,a+5,10,f-a),t.fillStyle="#284e3d",t.fillRect(e.x-4,n-15,60,15),t.fillRect(e.x-4,a,60,15)}t.fillStyle="#659f9b",t.fillRect(0,f,l,w-f),t.fillStyle="#d6e8c1",t.fillRect(0,f,l,5),t.strokeStyle="#a6d3c6",t.lineWidth=2;for(let e=-40;e<l+40;e+=62){const n=b*22%62;t.beginPath(),t.moveTo(e-n,465),t.lineTo(e+22-n,465),t.stroke()}t.save(),t.translate(m(),o==="ready"?s+Math.sin(b*3)*6:s),t.rotate(o==="playing"?Math.max(-.4,Math.min(.8,u/650)):-.08),t.fillStyle="#e5ac33",t.beginPath(),t.moveTo(-12,1),t.lineTo(-28,-8),t.lineTo(-21,12),t.fill(),d(-2,5,21,15,"#f6cf57"),d(10,-9,13,13,"#ffe58a"),d(-5,7,12,7+Math.sin(b*15)*2,"#e6ad39"),t.fillStyle="#d87930",t.fillRect(19,-8,14,7),d(14,-13,2.3,2.8,"#243d36"),t.restore()}let L=0;function j(e){const n=v?Math.min((e-v)/1e3,.035):0;v=e,Z(n),ee(),L=requestAnimationFrame(j)}document.addEventListener("visibilitychange",()=>{v=0});function q(e){document.documentElement.style.setProperty("--bg",e.colors.background),document.documentElement.style.setProperty("--fg",e.colors.text),document.documentElement.style.setProperty("--primary",e.colors.primary)}if(U("theme"))try{K().then(q).catch(()=>{}),Q(q)}catch{}window.addEventListener("pagehide",()=>{cancelAnimationFrame(L),_.disconnect()},{once:!0});A();B();L=requestAnimationFrame(j);
</script>
<style>:root{font:13px/1.4 system-ui,sans-serif;color-scheme:light dark;--bg: #172e29;--fg: #edf4e9;--primary: #efcc64}*{box-sizing:border-box}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}body{overflow:hidden;-webkit-user-select:none;user-select:none}#app{display:flex;flex-direction:column;padding:8px;gap:8px}.toolbar{display:flex;align-items:center;gap:8px;flex-shrink:0;min-width:0}.score{font-weight:750;font-variant-numeric:tabular-nums}.best{color:color-mix(in srgb,var(--fg) 65%,var(--bg));font-size:12px;margin-right:auto;white-space:nowrap}button{height:30px;padding:0 12px;border:1px solid color-mix(in srgb,var(--fg) 25%,var(--bg));border-radius:6px;font:inherit;font-weight:650;background:transparent;color:var(--fg);cursor:pointer}button:first-of-type{background:var(--primary);color:var(--bg);border-color:transparent}button:active{transform:translateY(1px)}button:focus-visible,canvas:focus-visible{outline:2px solid var(--primary);outline-offset:2px}.field{position:relative;flex:1;min-height:0;min-width:0;overflow:hidden;border-radius:6px;background:#d5eee9}canvas{display:block;width:100%;height:100%;touch-action:none}.message{position:absolute;inset:12% 8px auto;display:flex;align-items:center;flex-direction:column;gap:3px;pointer-events:none;color:#244b3d;text-align:center;text-shadow:0 1px #eef6e4}.message[hidden]{display:none}.message strong{font-size:clamp(15px,2.6vw,23px)}.message span{font-size:12px}@media(max-width:239px){#app{padding:4px;gap:4px}.toolbar{gap:5px}button{padding:0 8px}.best{font-size:11px}}@media(max-height:220px){#app{padding:4px;gap:4px}button{height:26px}.message{inset-block-start:8%;gap:0}.message strong{font-size:15px}.message span{font-size:11px}}
</style>
<meta name="napplet-type" content="my-napplet">
</head>
<body>
<!--
A napplet is an applet framed by a runtime: the runtime already shows this
napplet's name, so there is no title header here. The frame is working
surface from the first pixel, and the layout adapts to any frame size.
-->
<main id="app" class="app">
<div class="toolbar" role="toolbar" aria-label="Actions">
<button id="identityButton" type="button" title="Refresh identity">
<span aria-hidden="true">◎</span><span class="label">Identity</span>
</button>
<button id="storageButton" type="button" title="Save draft">
<span aria-hidden="true">▣</span><span class="label">Save</span>
</button>
<button id="outboxButton" type="button" title="Query outbox">
<span aria-hidden="true">⇅</span><span class="label">Query</span>
</button>
<button id="resourceButton" type="button" title="Load resource">
<span aria-hidden="true">⬇</span><span class="label">Resource</span>
</button>
<button id="notifyButton" type="button" title="Send notification">
<span aria-hidden="true">🔔</span><span class="label">Notify</span>
</button>
<output id="status" class="status status-idle" aria-live="polite">Ready</output>
</div>
<dl id="capabilities" class="domains" aria-label="Injected NAP domains"></dl>
<div class="workspace">
<section class="pane pane-draft" aria-label="Draft">
<label class="field">
<span class="field-label">Draft note</span>
<textarea id="noteInput" rows="3">Hello from my napplet.</textarea>
</label>
<div class="meta">
<span class="meta-key">identity</span>
<span id="identityValue" class="mono truncate">not loaded</span>
<span class="meta-key">storage</span>
<span id="storageValue" class="mono truncate">not saved</span>
</div>
</section>
<section class="pane pane-output" aria-label="Output">
<pre id="output" class="output" data-napplet-select="text">No events yet.</pre>
</section>
</div>
</main>
</body>
</html>
