This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

Built with my guy @hsmuyu ._.

What I Built

I love exploring. Trying new food, finding streets I've never walked, going out just to enjoy the day. But most of my days happen in one room, staring at a screen.

Part of it is being a student. Most fun things cost money: arcades, skiing, trips. And living on campus, even simple hobbies like gardening aren't really possible. So most of the time, the only option left is walking out the door.

But honestly, I barely walked anywhere for fun. I only go out when I have to: grab food, to buy groceries, to get through the week. Same route there & back. I'd lived here for months and still only knew the way to the canteen and the supermarket :')

When I tried to explain this to my teammate, it came down to games. I can spend hours wandering a game map for no real reason, because games reward you. You take a quest, you go somewhere, you get something back: a sound, a badge, a number going up. Every small step feels like an achievement. My walks had none of that. I'd go out, get what I needed, and come back. Nothing found, and nothing to show or brag about.

So we built footfolio. It turns the real world around you into a map, wherever you are: your campus, your hometown, a city you're visiting. And it rewards you for walking it.

The fog clears only where you actually walk, and the real cafes, parks or shops underneath appear as you uncover them. Gemma, running on your phone, gives you quests: hints about real places still hidden in the fog. You read one, put the phone away, and go look. If you're stuck for a while, a small light shows up to help, but only after you've tried.

When you find the place, the app notices. The quest completes, the place joins your map, and you earn your way toward badges: Explorer for the places you've uncovered, Local legend for a famous spot to eat, Landmark for a famous place to see.

It's small, but it's the exact same feeling that keeps me playing a game: I did something, and I have something to show for it.

Over time, the cleared map becomes a record of where your feet have been. That's the name: a folio of your footsteps. And the fog that's left tells you where to go discover next :D

Demo

Live: https://footfolio.onrender.com/

footfolio is made for phone. Open the link, allow location, and it starts wherever you are. Then go for a walk. Keep the screen on while you walk, since phones pause GPS when the screen locks.

Not outside right now? Add ?debug to the link and tap the map to walk. Debug mode drops you on our campus in Singapore. ?debug=10 walks ten times faster.

Gemma is optional. Turn it on from the quest card or Settings. It downloads once (about 800 MB, or 240 MB on phones without WebGPU), so Wi-Fi is a good idea. Without it, quests still work with plain clues.

Code

footfolio logo: a pixel-art sneaker

footfolio

Clear the fog, one walk at a time.

footfolio covers your neighbourhood map in fog. Walk outside and the fog clears where you go; the cafes, parks and shops underneath appear as you uncover them. A small open AI model, Gemma, runs on your phone and writes riddles about places still hidden in the fog, so you end up walking streets you have never tried. Over time the map becomes a record of everywhere you have been.

The screen is meant to be the shortest part: read a riddle, put the phone away, look around.

Built for the DEV "Hacktoberfest Open-Source AI Challenge: Week 1" (Touch Grass).

How it plays

Fog Each good GPS fix clears about 40 m around you. Your uncovered map is saved on the phone.
Places Real places from OpenStreetMap, within 2 km of you. They appear as gold pins when your walk uncovers them.
…

How I Built It

footfolio is a phone web app: Vite and plain TypeScript, Leaflet for the map, and OpenStreetMap for the tiles and the places. Almost everything runs on the phone. The fog is a grid of ~11 m squares drawn on a canvas; every good GPS reading clears the squares around you, and what you've cleared is saved in the browser.

Render: hosting the app, and feeding Gemma real places

The app is a Render Static Site. That matters more than it sounds: phones only share GPS with HTTPS pages, so it needs real hosting to work at all. The build command runs the test suite before building, so a broken build never goes live.

The places come from OpenStreetMap through the Overpass API, and that's where I needed a backend. Browsers can't call Overpass reliably: the public servers are slow, rate-limited, and want a header browsers aren't allowed to set. So there's one Render Web Service, server/places.ts, about 85 lines with one job: GET /places?lat&lng.

The heart of it is a few lines:

// server/places.ts (trimmed) const c = roundCentre({ lat, lng }); // round to ~550 m again: never an exact position const entry = await region(c.lat, c.lng); // cached for a week; one Overpass call per region send(200, entry.gz, { 'content-encoding': 'gzip', 'cache-control': 'public, max-age=86400' }); // ... console.warn('upstream failed:', String(e)); // the error only: no coordinates are ever logged 

The app sends a position already rounded to ~550 m, and the server rounds it again. It asks Overpass for the places within 2 km, filters them down to the ones worth finding, gzips the answer (a dense city goes from about 1 MB to 100 KB) and caches it for a week. No database, no accounts. When the official Overpass server went down on the day I built this, I added two mirrors it falls back to, and if they all fail, it serves the last copy it has.

That one service is why footfolio works wherever you are. The app loads a 2 km circle around you, and when you've walked 1.5 km away it quietly loads the next one.

Every riddle Gemma writes is about a place that came through Render. Render doesn't run the model, on purpose (more on that below), but it's what gives the model something real to write about.

Gemma: on the phone, writing riddles

The rule I set early: the code decides, Gemma writes. The code picks the hidden place, how far it is and when to give more help, and your GPS decides when you've found it. Gemma only writes the clues. A small model can't be trusted with game rules or map data, but it's good at turning "a cafe, 300 m away, near the park you found yesterday" into a riddle.

It runs in the browser, two ways:

  • With WebGPU: Gemma 3 1B (4-bit ONNX, ~800 MB) through Transformers.js.
  • Without WebGPU (older iPhones, many Android phones, in-app browsers): Gemma 3 270M (4-bit GGUF, ~240 MB) on the CPU through wllama, which is llama.cpp compiled to WebAssembly.

The second path exists because of a real moment: I turned Gemma on and my phone said "needs WebGPU", while my teammate's worked fine. Plenty of phones are like mine, and telling those users "no" felt wrong, so they get a smaller model on the processor instead. It's slower, so their clues are written ahead in the background.

I picked the models by testing them side by side. The 270M model on its own mostly copied my examples back or wrote as if it were the player. The 1B wrote 16 usable clues out of 16, at about a second each on a laptop GPU. Nothing downloads until you switch Gemma on, and the switch shows the size first.

Making a small model sound human (no fine-tuning)

My first riddles were robotic: "Head north-east to a cafe concealed by the fog." Correct, but it read like a GPS, and honestly, most people can't use "north-east" on a walk. I didn't fine-tune anything. Four things in src/questText.ts did the work.

1. Give it a character and a tiny job. Gemma doesn't play an assistant here; it plays the fog itself:

const RULES = 'You are the fog in a walking game. Speak to the player (you) in one short, playful sentence of at most 12 words. ' + 'Say what the hidden place is like, never its name. Do not give a direction, a distance or a number. Do not invent other places, people or stories.'; 

The direction moved out of the words entirely. The wisp on the map shows where to go, so Gemma only has to describe what a place feels like: something a small model is actually good at.

2. Show, don't tell. Small models follow examples far better than rules. So every prompt opens with two hand-written examples, sent as real back-and-forth chat turns, picked for the stage of the quest:

return [ { role: 'user', content: `${RULES}\n\n${ask(a[0])}` }, // "Hidden place: Swee Choon Dim Sum (restaurant). Give the first riddle." { role: 'assistant', content: a[1] }, // "Somewhere ahead, little baskets of dumplings are steaming all day." { role: 'user', content: ask(b[0]) }, { role: 'assistant', content: b[1] }, { role: 'user', content: ask(f) }, // the real quest ]; 

Sending them as separate turns mattered. When I put both examples in one message, both models just kept continuing the pattern instead of answering.

3. Feed it facts, one stage at a time. A quest has three clues. The first is a riddle. The second comes when the wisp appears, and it gets one more fact: the nearest place you've already found, so the clue can start from somewhere you know ("Past the library, follow the smell of steamed buns."). The third comes when you're close: "it's right around you, one last detail."

4. Never trust the answer. Everything Gemma writes goes through clean() before you see it. It keeps the first line, strips emoji, markdown and labels like "Clue:", and cuts to whole sentences. Then it throws the answer away if it:

  • uses a number we didn't give it
  • uses a compass word
  • talks as the player ("I'm ready…")
  • copies one of the examples
  • gives the place away

That last check is the subtle one. Banning the whole name would be too strict ("Bedok Reservoir Park" would ban the word park), so only the distinctive words are banned:

// "Bedok Reservoir Park" → bans "bedok" and "reservoir", not "park" (the kind already says that) export const giveaways = (f) => { const said = `${f.what} ${f.near ?? ''}`.toLowerCase(); return f.hidden.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 4 && !said.includes(w)); }; 

Every rule in that list came from a real bad answer. Gemma gets up to five tries (with some randomness, so each try is different), and every rejected answer is logged so I could keep tuning the checks. A plain line shows instantly while Gemma writes, and its clue replaces it a second or two later. If all five fail, the plain line simply stays. The game never waits on the model.

Why Does Open Innovation Matter?

For footfolio, open isn't a nice extra. It's the reason the idea can exist at all.

A walking game is, by nature, a stream of where you are. Building the riddles on a closed API would mean sending each player's position, and the places around them, to someone else's server on every quest. With Gemma, that never happens:

  • Your location stays private. Gemma runs in the browser on your phone, so your path, your fog and your finds never leave it. The only thing that goes out is a point rounded to ~550 m, to the places service we run.
  • No third party. No AI company sees where anyone walks, what they found, or what they were looking for.
  • Nothing to pay per quest. No API key to hide, no charge per riddle. That matters for who this is for: walking is the free option, and an app that costs money every time it speaks wouldn't fit that. The only running cost is one small places service on Render.
  • No lock-in. The model is one line of config. I swapped the 270M for the 1B when the riddles weren't good enough, then brought a 270M build back alongside it for phones without WebGPU. With a closed API, I'd get whichever models the vendor decides to offer.
  • It keeps going when the signal drops. Once the page is open, GPS, the fog, the quests and Gemma all run on the phone, and the places for your area are already saved. Lose signal halfway down a back street and the game carries on; only new map tiles wait for the connection.

Open wasn't the easy path, though. Google's own web-ready Gemma 3 models are gated behind a Hugging Face login, and a public website has nowhere to keep that key, so I used the openly published ONNX and GGUF builds instead. A small model also needs guidance a big API wouldn't: the narrator prompt, the examples, the checks on every answer. And the download, 240 to 800 MB, is a real ask, which is why it's opt-in and shows its size first.

A hosted model would have written better riddles with no download. But it would have meant a server watching everyone's walk, and for a game that is literally a map of where you go, that's the wrong trade.

The data is open too. Every place in footfolio comes from OpenStreetMap, mapped by volunteers, and when the official Overpass server went down mid-build, the open ecosystem had mirrors to fall back on.

My Agent Session

This is the session where I worked on the Gemma part. First checking if it could run on a phone at all, then getting it to write clues without saying the place's name :)

Gemma on the phone: from 'is that possible?' to riddles that hide the place
You

i plan to have quest generated when the user is at one place
make it top upper right corner, thinking of webgpu with gemma is that possible?

Agent
▸ Bash {"command": "git ls-files | grep -v -E 'public/|\\.png$' && cat package.json ...
$ {"command": "git ls-files | grep -v -E 'public/|\\.png$' && cat package.json && git diff package.json && sed -n '1,200p' PLAN.md | grep -n -i -E 'phase 6|quest|gemma|webgpu|llm|ai' ", "description": "List project files, deps, and quest notes in plan"}
.github/workflows/deploy.yml .gitignore CLAUDE.md PLAN.md README.md index.html package-lock.json package.json src/config.ts src/coverage.ts src/debugWalk.ts src/discovery.ts src/flags.ts src/fog.ts src/fogLayer.ts src/gps.ts src/hud.ts src/icons.ts src/main.ts src/map.ts src/me.ts src/places.ts src/placesLayer.ts src/questLayer.ts src/quests.ts src/sound.ts src/storage.ts src/style.css src/theme.ts src/today.ts src/types.ts src/ui.ts src/vite-env.d.ts tests/browser.mjs tests/design.test.ts tests/fog.test.ts tests/gps.test.ts tests/places.test.ts tests/quests.test.ts tests/storage.test.ts tests/today.test.ts tools/fetch-places.mjs tsconfig.json vite.config.ts { "name": "fog-walk-map", "private": true, "type": "module", "engines": { "node": ">=24" }, "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "typecheck": "tsc", "test": "node tests/fog.test.ts && node tests/storage.test.ts && node tests/places.test.ts && node tests/design.test.ts && node tests/gps.test.ts && node tests/today.test.ts && node tests/quests.test.ts", "test:browser": "node tests/browser.mjs" }, "dependencies": { "@fontsource/atkinson-hyperlegible-next": "^5.3.0", "leaflet": "^1.9.4" }, "devDependencies": { "@types/leaflet": "^1.9.22", "@types/node": "^24.19.1", "puppeteer-core": "^25.12.0", "typescript": "^7.0.2", "vite": "^8.3.4" } } diff --git a/package.json b/package.j … [13226 chars trimmed]
▸ Bash {"command": "sed -n 175,260p PLAN.md; ls ../walking-maxxing; grep -rn -i -E '...
$ {"command": "sed -n 175,260p PLAN.md; ls ../walking-maxxing; grep -rn -i -E 'gemma|webgpu|model|llm' ../walking-maxxing --include=*.md | head -40", "description": "Read Phase 6 plan and quest AI design notes"}
**6. Simple quests (no AI)** ✅ built, awaiting your check. Branch `feat/add-simple-no-ai-related-side-quests`. The point is to test whether a quest system sits on the map cleanly, before the open model writes quests (that design lives in `../walking-maxxing`). Two quests, made by code, no AI. What the map already gives quests, so no new map API is needed: - Every place in `places.json` has a stable OpenStreetMap id (`placeKey`). - `discovery.ts` has one goal slot, `currentGoal()`, which the chip and both rings render. - `discovery.check()` knows every new find and already adds a detail line to the toast when the goal completes. The same path carries "Quest done", so a quest never needs a second toast. - The finding to record: the earlier idea of an `api.ts` with `where / isRevealed / reached / marker` isn't needed. Quests plug into `discovery.ts` and the goal slot, and "reached" is the same rule as "found". The quests (they alternate: reach, find, reach, find…): - **Reach**: - Target: a place not yet found, 150–400 m from you. If none is in that band, the nearest unfound place. - A quest marker shows the spot through the fog. The name stays hidden until you get there. - Done when that place is found, i.e. its cell clears as you arrive (~40 m). There's no separate arrival radius. - Chip: "Reach the marked spot, 240 m" (rounded to 10 m). Ring progress is 1 − distance now / distance at start. - **Find**: - "Find 2 new places". - Counts announced finds since the que … [4924 chars trimmed]
▸ ToolSearch {"query": "select:WebSearch", "max_results": 1}
{ "query": "select:WebSearch", "max_results": 1 }
▸ Bash {"command": "grep -n -E 'top:|right:|left:|bottom:|^\\.[a-z-]+|^#[a-z-]+' src...
$ {"command": "grep -n -E 'top:|right:|left:|bottom:|^\\.[a-z-]+|^#[a-z-]+' src/style.css | head -80; grep -n -i 'leaflet-control\\|zoomControl\\|attribution' src/map.ts", "description": "Find what already occupies screen corners"}
17:#map { position: fixed; inset: 0; } /* fixed, so mobile toolbar resizing can't scroll the page */ 22:#top { 23: position: fixed; left: calc(16px + env(safe-area-inset-left)); right: calc(16px + env(safe-area-inset-right)); 24: top: calc(12px + env(safe-area-inset-top)); z-index: 1000; 27:#chip, #stats, #status, #hint { font-family: var(--ui); color: var(--daylight); background: var(--panel); } 28:#top button { touch-action: manipulation; cursor: pointer; } 30:#chip:active, #lastFind:active, #soundSwitch:active { background-image: linear-gradient(rgba(238, 243, 248, 0.12), rgba(238, 243, 248, 0.12)); } /* a tint, not a shadow: the focus ring keeps its shadow while pressed */ 32:#chip { 37:#chip { max-width: 100%; } 38:#chipText { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* a long label (large text) shortens instead of pushing the chip off screen */ 39:.chev, .chev-right { flex: none; fill: none; stroke: var(--dusk); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; } 40:.chev { transition: transform .18s ease-out; } 41:#chip[aria-expanded="true"] .chev { transform: rotate(180deg); } 44:.ring { flex: none; display: block; } 45:.ring circle, .ring path { fill: none; stroke-width: 3.5; } 46:.ring-track { stroke: var(--dusk); opacity: 0.35; } 47:.ring-arc { stroke: var(--first-light); stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: stroke-dashoffset .6s ease-ou … [5797 chars trimmed]
▸ WebSearch {"query": "Gemma in browser WebGPU MediaPipe LLM Inference web Gemma 3n trans...
{ "query": "Gemma in browser WebGPU MediaPipe LLM Inference web Gemma 3n transformers.js 2026", "mode": "standard" }
Web search results for query: "Gemma in browser WebGPU MediaPipe LLM Inference web Gemma 3n transformers.js 2026" Links: [{"title":"LLM Inference guide for Web","url":"https://developers.google.com/edge/mediapipe/solutions/genai/llm_inference/web_js"},{"title":"LLM Inference guide for Web","url":"https://developers.google.com/edge/mediapipe/solutions/genai/llm_inference/web_js?authuser=7"},{"title":"LLM Inference guide for Web","url":"https://developers.google.com/edge/mediapipe/solutions/genai/llm_inference/web_js?authuser=2"},{"title":"LLM Inference guide for Web","url":"https://developers.google.cn/edge/mediapipe/solutions/genai/llm_inference/web_js"},{"title":"web js","url":"https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/web_js?authuser=0"},{"title":"LLM Inference guide for Web","url":"https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/web_js?authuser=8"},{"title":"gemma 3 browser react","url":"https://zenn.dev/shiba_hiro/articles/gemma-3-browser-react"},{"title":"DEV Community","url":"https://dev.to/gemma_322a35bf7fa2afab/how-we-built-a-private-on-device-ai-that-runs-entirely-in-your-browser-30"},{"title":"WebLLM: LLM-Inferenz direkt im Browser mit WebGPU","url":"https://zeli.app/de/story/49536411"}] The search turned up solid information on Google's MediaPipe route for running Gemma in the browser, but nothing specific to Gemma 3n with Transformers.js in 2026. **Official MediaPipe route (strongest source)** - Google's web guide say … [1996 chars trimmed]
▸ WebSearch {"query": "WebGPU mobile support Safari iOS 26 Chrome Android 2026", "mode": ...
{ "query": "WebGPU mobile support Safari iOS 26 Chrome Android 2026", "mode": "standard" }
Web search results for query: "WebGPU mobile support Safari iOS 26 Chrome Android 2026" Links: [{"title":"1. https://appdevelopermagazine.com/ios","url":"https://appdevelopermagazine.com/WebGPU-in-iOS-26/"},{"title":"WebGPU in iOS 26","url":"https://appdevelopermagazine.com/webgpu-in-ios-26/amp/"},{"title":"1. https://appdevelopermagazine.com/ios","url":"https://appdevelopermagazine.com/webgpu-in-ios-26/"},{"title":"WebGPU vs WebGL for Games (2026)","url":"https://app.cinevva.com/guides/webgpu-vs-webgl-games"},{"title":"WebGPU बनाम WebGL Games के लिए (2026)","url":"https://app.cinevva.com/hi/guides/webgpu-vs-webgl-games"},{"title":"browser support","url":"https://abratabia.com/webgpu/browser-support.php"},{"title":"WebGPU vs WebGL pour les jeux (2026)","url":"https://app.cinevva.com/fr/guides/webgpu-vs-webgl-games"},{"title":"WebGPU vs WebGL para jogos (2026)","url":"https://app.cinevva.com/pt-BR/guides/webgpu-vs-webgl-games"},{"title":"WebGPU 与 WebGL 游戏对比(2026)","url":"https://app.cinevva.com/zh-CN/guides/webgpu-vs-webgl-games"}] WebGPU is now available on iOS in Safari, and Chrome on Android supports it, though the sources I found are mostly secondary guides and don't agree on every detail. **Safari on iOS and iPadOS:** Apple's mobile devices were the last major platform without WebGPU until iOS 26. With iOS 26, Safari gains full WebGPU support, unifying Apple's ecosystem across desktops, tablets, and phones. Apple's implementation sits on Metal, and one guide notes that … [1278 chars trimmed]

Yes, it's possible, and it suits the project: Gemma would run on the phone, so location still never leaves the device. There are a few catches.

How it would work
- Google's MediaPipe web package (@mediapipe/tasks-genai) runs Gemma in the browser on WebGPU. Google publishes "-Web" builds of Gemma 3 (270M up to 27B), Gemma 3n E2B/E4B and Gemma 4 E2B/E4B. For a phone, start with the smallest: Gemma 3 270M or 1B.
- WebGPU now works in Safari on iOS 26 and in Chrome on Android. It doesn't work in Firefox on Android yet, and older phones lack it. Those users would keep today's code-made quests as the fallback.

The catches
1. Download size. The model is roughly hundreds of MB for the small ones and several GB for 3n/4. It can't load on first open over mobile data. Make it an opt-in download, cache it, and host it on Hugging Face. GitHub Pages won't serve files over 100 MB.
2. Memory. iOS kills tabs that use too much memory, so the bigger models are risky on phones.
3. Speed. Writing a short quest takes a few seconds. Start generating the next quest while the current one is still going, so the text is ready when you arrive.
4. Made-up facts. A small model will invent things like "the famous mural". Keep the current split:
- Code: picks the target, checks completion by GPS (the existing reach/find rule) and handles anything that can be checked.
- Gemma: only writes the wording, from the place name, its type and nearby unfound places.
- Output: cap its length, show it with textContent, and use a template if the output is bad.

Top-right corner

The top bar (#top) spans the full width. The chip is on the left with max-width: 100%, so a long label or large text would run into a top-right card. The stats card also opens full width underneath. Two decisions follow:
- I'd cap the chip's width so the top-right card has room.
- In Phase 6 the quest took over the chip ("one goal at a time"). If the quest moves to the top-right, the chip can go back to showing the daily goal. That change is fine as long as you mean it.

Where it hooks in: finishing a quest already starts the next one after QUEST_NEXT_MS (in discovery.ts). That's where generation would start, with the place you're at as input.

Next step I'd suggest: before building any UI, make a small ?llm test page that loads Gemma 3 1B and times one quest prompt on your actual phone. If it's too slow or crashes, we learn that before building the rest.

PLAN.md says the AI quest design lives in ../walking-maxxing, but I couldn't find that folder next to this one.

Sources:
- MediaPipe LLM Inference guide for Web
- WebGPU in iOS 26
- WebGPU browser support

Show more
You

yes do it a serverless one must use gemma others you decide, output your decision to a md and do ir

Agent

I'll read the quest, discovery, HUD and wiring code first.

▸ Bash {"command": "wc -l src/*.ts index.html tests/*.mjs tests/*.ts && cat src/ques...
$ {"command": "wc -l src/*.ts index.html tests/*.mjs tests/*.ts && cat src/quests.ts src/discovery.ts", "description": "Show file sizes, quests and discovery code"}
 45 src/config.ts 21 src/coverage.ts 20 src/debugWalk.ts 155 src/discovery.ts 3 src/flags.ts 47 src/fog.ts 153 src/fogLayer.ts 40 src/gps.ts 104 src/hud.ts 20 src/icons.ts 69 src/main.ts 20 src/map.ts 62 src/me.ts 82 src/places.ts 46 src/placesLayer.ts 19 src/questLayer.ts 85 src/quests.ts 39 src/sound.ts 53 src/storage.ts 15 src/theme.ts 41 src/today.ts 3 src/types.ts 25 src/ui.ts 1 src/vite-env.d.ts 66 index.html 1153 tests/browser.mjs 57 tests/design.test.ts 79 tests/fog.test.ts 56 tests/gps.test.ts 128 tests/places.test.ts 100 tests/quests.test.ts 73 tests/storage.test.ts 64 tests/today.test.ts 2944 total // The two test quests, no AI: "reach" (walk to a marked spot) and "find" (uncover new places). Pure logic plus a small store, so Node can test it. // Both finish the same way, by finding places, so quests need nothing from the map beyond what discovery already knows. import { dist } from './fog.ts'; import { placeKey, type Place } from './places.ts'; import { QUEST_MIN_M, QUEST_MAX_M, QUEST_FIND, QUEST_KEY, REVEAL_RADIUS } from './config.ts'; import type { Goal } from './today.ts'; import type { LatLng } from './types.ts'; interface Base { seq: number; done: boolean } // seq 0, 1, 2…: even is a reach quest, odd is a find quest export interface Reach extends Base { kind: 'reach'; key: string; lat: number; lng: numb … [14497 chars trimmed]
▸ Bash {"command": "cat src/hud.ts src/main.ts index.html src/config.ts src/questLay...
$ {"command": "cat src/hud.ts src/main.ts index.html src/config.ts src/questLayer.ts src/ui.ts src/today.ts src/flags.ts vite.config.ts .github/workflows/deploy.yml; sed -n 1,40p src/style.css", "description": "Show HUD, wiring, markup, config and styles"}
// The progress chip, the stats card it opens, the sound switch, and the first-open hint. DOM only. // Everything shown is set with textContent; the only markup built here is static (the pin icons). import { GROUP_LABELS, discHtml } from './icons.ts'; import { typeLabel, type Group, type Place } from './places.ts'; import type { Goal } from './today.ts'; const $ = (id: string) => document.getElementById(id)!; const GROUPS: Group[] = ['food', 'shop', 'outdoors', 'other']; export interface Stats { goal: Goal; daily: Goal; // goal = what the chip shows (a quest's, or the daily one when there is none); daily = today's count found: number; total: number; groups: Record<Group, { found: number; total: number }>; percent: string; fraction: number; // how much of the neighbourhood is explored: "0.10%" and 0.001 last: Place | null; } // A bar that is never invisible once there is something to show (a stub), with the exact number always written beside it. const stub = (part: number, any: boolean) => (any ? Math.max(0.05, Math.min(1, part)) : 0); const setBar = (el: HTMLElement, part: number, any: boolean) => { el.style.transform = `scaleX(${stub(part, any)})`; }; // The arc is a circle of length 1 (pathLength in the markup), so progress 0..1 is simply the dash offset 1..0. function setRing(svg: Element, goal: Goal) { const arc = svg.querySelector('.ring-arc') as SVGElem … [25262 chars trimmed]

Before deciding on a runtime, I need to know which Gemma web builds a browser can download without a Hugging Face login. I'll check the model listings.

▸ Bash {"command": "for m in litert-community/Gemma3-1B-IT litert-community/gemma-3-...
$ {"command": "for m in litert-community/Gemma3-1B-IT litert-community/gemma-3-270m-it litert-community/gemma-4-E2B-it-litert-lm google/gemma-3-1b-it onnx-community/gemma-3-1b-it-ONNX onnx-community/gemma-3-270m-it-ONNX onnx-community/gemma-3-1b-it-ONNX-GQA; do echo \"== $m\"; curl -s \"https://huggingface.co/api/models/$m\" | node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{const j=JSON.parse(s);console.log(\"gated:\",j.gated,\"lib:\",j.library_name,\"err:\",j.error||\"\");console.log((j.siblings||[]).map(f=>f.rfilename).filter(f=>/onnx|task|litertlm|\\.bin/.test(f)).slice(0,20).join(\"\\n\"))}catch(e){console.log(s.slice(0,200))}})'; done", "description": "Check gating and files of Gemma web model repos"}
== litert-community/Gemma3-1B-IT gated: auto lib: litert-lm err: Gemma3-1B-IT_multi-prefill-seq_f32_ekv1280.task Gemma3-1B-IT_multi-prefill-seq_f32_ekv4096.task Gemma3-1B-IT_multi-prefill-seq_q4_block128_ekv1280.task Gemma3-1B-IT_multi-prefill-seq_q4_block128_ekv4096.task Gemma3-1B-IT_multi-prefill-seq_q4_block32_ekv1280.task Gemma3-1B-IT_multi-prefill-seq_q4_block32_ekv4096.task Gemma3-1B-IT_multi-prefill-seq_q4_ekv2048.task Gemma3-1B-IT_multi-prefill-seq_q4_ekv4096.litertlm Gemma3-1B-IT_multi-prefill-seq_q8_ekv1280.task Gemma3-1B-IT_multi-prefill-seq_q8_ekv2048.task Gemma3-1B-IT_multi-prefill-seq_q8_ekv4096.task Gemma3-1B-IT_q4_ekv1280_mt6989.litertlm Gemma3-1B-IT_q4_ekv1280_mt6991.litertlm Gemma3-1B-IT_q4_ekv1280_mt6993.litertlm Gemma3-1B-IT_q4_ekv1280_sm8550.litertlm Gemma3-1B-IT_q4_ekv1280_sm8650.litertlm Gemma3-1B-IT_q4_ekv1280_sm8750.litertlm Gemma3-1B-IT_q4_ekv1280_sm8850.litertlm Gemma3-1B-IT_q8_ekv1280_Google_Tensor_G5.litertlm Gemma3-1B-IT_q8_ekv1280_Google_Tensor_G6.litertlm == litert-community/gemma-3-270m-it gated: auto lib: litert-lm err: gemma3-270m-it-q4_0-web.task gemma3-270m-it-q8-web.task gemma3-270m-it-q8.litertlm gemma3-270m-it-q8.mediatek.mt6991.litertlm gemma3-270m-it-q8.mediatek.mt6993.litertlm gemma3-270m-it-q8.qualcomm.sm8550.litertlm gemma3-270m-it-q8.qualcomm.sm8650.litertlm gemma3-270m-it-q8.qualcomm.sm8750.litertlm gemma3-270m-it-q8.qualcomm.sm8850.litertlm gemma3-270m-it-q8.task == litert-community/gemma-4-E2B-it-litert-lm gated: false lib: l … [1522 chars trimmed]

The Gemma 3 builds for MediaPipe are gated, meaning the browser would need a Hugging Face login. The Gemma 4 E2B web build and the ONNX Gemma 3 builds are open. Next I'll check their file sizes.

▸ Bash {"command": "for m in litert-community/gemma-4-E2B-it-litert-lm onnx-communit...
$ {"command": "for m in litert-community/gemma-4-E2B-it-litert-lm onnx-community/gemma-3-270m-it-ONNX onnx-community/gemma-3-1b-it-ONNX-GQA; do echo \"== $m\"; curl -s \"https://huggingface.co/api/models/$m?blobs=true\" | node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{const j=JSON.parse(s);console.log(\"license:\",(j.cardData||{}).license);for(const f of j.siblings) if(/web|q4f16|q4\\.|model_q4|tokenizer.json|config.json/.test(f.rfilename)) console.log((f.size/1e6).toFixed(0).padStart(6),\"MB\",f.rfilename)})'; done; curl -sI https://huggingface.co/onnx-community/gemma-3-270m-it-ONNX/resolve/main/config.json | grep -i -E 'access-control|^HTTP'", "description": "Get model file sizes, licenses and CORS headers"}
== litert-community/gemma-4-E2B-it-litert-lm license: apache-2.0 2008 MB gemma-4-E2B-it-web.litertlm 2004 MB gemma-4-E2B-it-web.task == onnx-community/gemma-3-270m-it-ONNX license: gemma 0 MB config.json 0 MB generation_config.json 0 MB onnx/model_q4.onnx 323 MB onnx/model_q4.onnx_data 0 MB onnx/model_q4f16.onnx 273 MB onnx/model_q4f16.onnx_data 20 MB tokenizer.json 0 MB tokenizer_config.json == onnx-community/gemma-3-1b-it-ONNX-GQA license: undefined 0 MB config.json 0 MB generation_config.json 0 MB onnx/model_q4.onnx 859 MB onnx/model_q4.onnx_data 0 MB onnx/model_q4f16.onnx 763 MB onnx/model_q4f16.onnx_data 20 MB tokenizer.json 0 MB tokenizer_config.json HTTP/1.1 307 Temporary Redirect Access-Control-Max-Age: 86400 Access-Control-Allow-Origin: https://huggingface.co Access-Control-Expose-Headers: X-Repo-Commit,X-Request-Id,X-Error-Code,X-Error-Message,X-Total-Count,ETag,Link,Accept-Ranges,Content-Range,X-Linked-Size,X-Linked-ETag,X-Xet-Hash
▸ Bash {"command": "sed -n 51,64p src/style.css; sed -n 104,160p src/style.css; cat ...

Related posts