flutter
Flutter는 wger용 피트니스·운동 앱입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기make a running route to form words/shapes
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Type a word or upload a logo, drop a pin anywhere in the world, pick a distance, and get a running route whose GPS trace draws it. Load the GPX into Strava, Garmin or your watch, run the line, and the orange map in your activity is the drawing.
Everything is plain geometry and graph search on OpenStreetMap data. No AI API is needed (see Do I need an AI API?).README 전문 보기 ↗Type a word or upload a logo, drop a pin anywhere in the world, pick a distance, and get a running route whose GPS trace draws it. Load the GPX into Strava, Garmin or your watch, run the line, and the orange map in your activity is the drawing.
words / image + a spot on the map + ~5K / ~10K / half / your own distance
│
▼
strokes → sized to the local blocks → laid onto real streets → scored → GPX
Everything is plain geometry and graph search on OpenStreetMap data. No AI API is needed (see Do I need an AI API?).
| Path | What it is |
|---|---|
engine/ | Python package runmapper_engine: the route engine, a CLI, and a FastAPI service |
engine/runmapper_engine/font.py | The grid font for typed phrases and the walk optimiser that draws each letter as one retraced line |
engine/runmapper_engine/image.py, svgin.py, raster.py | Image and SVG tracing: filled outline or single centreline, chosen by how bold the shape is (thinning and contour tracing in plain numpy) |
engine/runmapper_engine/pipeline.py | The end-to-end plan: sizes from the distance bucket, street fetch, placement scan, snapping, verdict |
engine/runmapper_engine/api.py | POST /api/plan streams progress then the result; GET /api/plan/{job} gives a page that lost the stream the lines it missed; POST /api/estimate; GET /api/health |
web/ | Next.js 16 app: the word on a split-flap board (each letter turns in through the letters before it; a long word takes a second row at a readable size; or a shape drawn on a pad by finger or mouse, or an uploaded image), the MapLibre map (night, day or satellite; the route drawn in start to finish with direction chevrons; replay, recenter, target and zoom keys, all one height, the replay key latching down as a pause key while a run plays; on the satellite map the replay is a flyover: the camera flies the course first person over the terrain, following the tip, with the line drawn twice as thick for it, then eases back up to the whole route; while a replay runs, a fader scrubs to any point of it and a speed key steps through 1×, 2×, 2.5× and 3×), the Loop switch as a rocker on a screwed plate, a knob that slides to the side in use, carrying a lamp that lights when it is on (units follow the phone's locale, miles in the US and kilometres elsewhere, and the bib shows both), the deck's own noises for every key, switch and falling flap (synthesised in the browser, nothing downloaded; the key in the header quiets them and the choice is remembered), the start key built as every other key on the deck is (an orange face with a fillet on a stepped wall; hit, it stays down a moment and the stopwatch dissolves in over it), a stopwatch while the engine searches, and on the map the search itself: lines of light that leave the pin and run the streets outwards a few at a time, keeping on at junctions and now and then taking a side street, never turning back, until the first answer comes (the streets come from the basemap's own vector tiles, joined at their crossings; see web/src/lib/pulse.ts), the wordmark in the header (DRAWMYRUN drawn as one route with the green start dot for the period) etched into the deck, the answers as a pile of race bibs (distance as the bib number, the verdict stamped on, the GPX and GIF on the tear-off stub; the next answer is the whole bib just behind, so dragging the front one aside peeks at it and a throw either way brings it forward, or tap a band or use the arrow keys), the cue sheet as a wrist pace band, and a pin set by tap or link named after the nearest place |
web/api/index.py, web/scripts/vercel-install.sh, web/vercel.json | The engine as a Vercel Python function inside the same project |
engine/Dockerfile, engine/modal_app.py | Optional: host the engine somewhere else |
You need Python 3.11+ and Node 20+.
1. The engine (API) — terminal one
cd engine
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
uvicorn runmapper_engine.api:app --reload --port 8000
Check it: open http://localhost:8000/api/health.
2. The web app — terminal two
cd web
npm install
npm run dev # proxies /api to the engine on port 8000
Open http://localhost:3000, type RUN on the split-flap board (POST /api/estimate checks it fits the distance as you type, and returns the strokes for anyone who wants them), search a place (or click the map), pick 5K, and hit Draw my run. A URL can carry the setup (?t=RUN&lat=…&lon=…&d=5k&loop=1&s=line); opening it fills the form in, and nothing runs until Draw my run. GIF on the bib's stub renders the route drawing itself in on a hidden 1280 × 720 map of its own (16:9 for X and the like, with the word, the distance and the town large in a wash along the top, the site top right, and the bottom corners left clear for X's GIF badge; the line and its markers are drawn twice as thick as on screen, since the GIF is seen small on a phone), encodes it in the browser with gifenc and, on a desktop, downloads it; nothing on screen moves while it works. The finished frame holds, the line melts away leaving the start dot, and the drawing runs again, so the loop comes round smoothly. On a phone the key reads Share GIF and hands the file to the share sheet with nothing else attached (pick X or Messages there and the GIF is in the post); the sheet has to be asked for by the tap itself and a render outlasts a tap, so the first tap renders, the key then reads Ready, and the next tap opens the sheet; the render is kept, so the second tap never renders again. Page views go to Vercel Analytics (@vercel/analytics; turn it on for the project in the Vercel dashboard). Every frame after the first carries only the pixels that changed, so a file is a fraction of a megabyte per second, and it is kept under 5 MB (what X accepts from a phone; 15 MB from a browser) by rendering again smaller and shorter if it ever runs over. Files are named after the run, e.g. RUN-3.40mi-San-Francisco.gpx (the city from a reverse lookup of the start). The search runs at the engine, and a phone that switches apps puts the page to sleep and cuts the stream; the search carries on regardless, and every line it says is kept on record at the engine under a job id the page sent along (for a quarter of an hour after it ends; on Vercel the record is also put on the Runtime Cache, so whichever worker the page's next request lands on can hand it out), so when the page comes back it asks for the lines it missed and waits there for the rest: switching away costs nothing. If the record is not there (another worker answered, or too long has passed), the page keeps whatever had arrived, or restarts the search once on its own; otherwise it offers Try again. On the map, Target shows the shape the route is trying to draw and Recenter brings the whole route back on screen. The CLI takes the same options, e.g. runmapper "RUN" --lat 37.752 --lon -122.492 --bucket 10k --style outline. To use an engine running somewhere else, set in (see ).
Command line, no web app
cd engine
runmapper "HELLO" --lat 40.7410 --lon -73.9897 --bucket 10k --out out/ # Flatiron, New York
runmapper logo.png --lat 51.5220 --lon -0.1250 --bucket 10k --out out/ # Bloomsbury, London
runmapper "SF" --lat 37.7647 --lon -122.4270 --bucket 5k --out out/ # Dolores Park, San Francisco
Writes out/<name>-<bucket>.gpx, a .json with the stats and cue sheet, and a .png preview (streets grey, target dashed blue, route Strava orange).
Tests
cd engine && pytest # offline: synthetic street grid, font, tracing
cd web && npm run lint && npx tsc --noEmit && npm run build
One Vercel project runs both halves: the Next.js app, and the engine as a Python function behind /api. (Vercel allows Python functions a 500 MB bundle, streamed responses and 300 s per request, which is what the engine needs.)
web. Nothing else to configure.main redeploys.drawmy.run and point the domain's DNS at Vercel as it instructs. The repository can be renamed to match at any time: the engine install step reads the repository name Vercel passes at build time.What happens in the build: Vercel's Python step reads web/pyproject.toml, which runs web/scripts/vercel-install.sh; that script installs the engine package from this repository at the commit being built, so the function and the page always match. web/vercel.json sets the function's time limit. Street data is cached in the function's /tmp while an instance stays warm; the first request in a new area waits for Overpass.
The engine settings below can be set as environment variables on the Vercel project (Settings → Environment Variables, then redeploy).
| Variable | Default | Meaning |
|---|---|---|
RUNMAPPER_CORS_ORIGINS | * | Comma-separated origins allowed to call the API (only matters when the API is on another host) |
RUNMAPPER_MAX_JOBS | 2 | Routes computed at the same time per instance; extra requests wait |
RUNMAPPER_CACHE | .cache (/tmp/runmapper-cache on Vercel) | Directory for cached street data |
RUNMAPPER_OVERPASS_MIRRORS | four public mirrors | Comma-separated Overpass endpoints, asked in order |
RUNMAPPER_OVERPASS_STAGGER | 12 | Seconds to wait for a mirror before also asking the next one; the first answer wins |
RUNMAPPER_ELEVATION | 1 | Set 0 to skip the elevation lookup |
The web app accepts NEXT_PUBLIC_MAP_STYLE to swap the basemap (default: OpenFreeMap's Positron, no key) and NEXT_PUBLIC_API_URL to talk to an engine hosted elsewhere instead of its own function.
Useful for a bigger machine or a street cache that survives between requests. Either way, set NEXT_PUBLIC_API_URL on the Vercel project to the engine's URL and redeploy.
Railway (Dockerfile, click-through): New Project → Deploy from GitHub repo → set Root Directory to engine (Railway builds engine/Dockerfile) → add a Volume at /cache → under Networking generate a public domain. Render and Fly.io work the same way with the same Dockerfile.
Modal (scales to zero): pip install modal && modal setup && modal deploy engine/modal_app.py prints the URL. Or run the workflow in .github/workflows/deploy-engine.yml from the Actions tab after adding MODAL_TOKEN_ID and MODAL_TOKEN_SECRET as repository secrets.
! ? - . ' +.Typical run: the first route in about 7 s and all three in about 20 s once an area's streets are cached, plus the Overpass wait (10–30 s) the first time an area is used. The cache is written in the background as a pickle, so the plan never waits for it.
No. Nothing in the pipeline is a language or vision model: the font is data, tracing is classical image processing (thresholding, contours, skeletonisation), placement is a search, snapping is Dijkstra, and the verdict is a pixel overlap. It runs the same every time and costs nothing per request beyond compute.
Where a model could help later, as an optional extra:
Neither is needed for words or for logos that are already clean shapes.
RUNMAPPER_OVERPASS_MIRRORS at a paid instance.positron by day, which is the default, and the dark style at night, which a phone set to dark gets until a basemap is picked by hand; override the styles with NEXT_PUBLIC_MAP_STYLE_NIGHT and NEXT_PUBLIC_MAP_STYLE); the satellite view is Esri World Imagery with Esri's road and place-name reference tiles, and its flyover stands the imagery on elevation from the AWS Terrain Tiles (Mapzen terrarium tiles; attribution shown on the map); street data © OpenStreetMap contributors (ODbL).NEXT_PUBLIC_API_URLweb/.env.local.env.exampleprovisional); a better fit nearby replaces it, and a great fit at the pin ends that band early. A run starts at the pin when the drawing is within a quarter mile by street and the walk-on fits the distance (it is built into the route); otherwise it starts at the drawing, with the distance from the pin reported.message, shown on the bib). A word laid along the north–south streets (the only way its stems land on streets on some long blocks) is as good a drawing as any, so it keeps its verdict; the answer just notes that it reads sideways with north up.