BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2 或任何藍牙控制器,搭配 MyWhoosh、Rouvy、Zwift 等。iOS、Android、Windows、macOS 均可免費試用。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗GPX tracks rendered as interactive 3D terrain, entirely in the browser.
The logo is a crossing-signal runner sprinting for a glass of salpicón: a short,
wide "!" whose bar is the glass, with a bent straw. It is drawn as LED beads by
src/mark.js, and it doubles as a stoplight on the record screen. Waiting for
GPS or paused, the figure stands and blinks amber. Moving, it steps between two
strides while the glass fills, one glass per kilometre. On Finish, the glass is
full. The full glass is the main logo.
Colours are the tokens at the top of src/ui.css: a leche rosada ground, green
for every action, mango for the wait states, and red only as juice, straw and the
finish point. The dark variant follows the system setting. brand/ holds the
master SVG, the favicon and the touch icon; the Android launcher layers and splash
screens are rendered from the same geometry.
Each route is draped over real satellite imagery and elevation data fetched at
view time, drawn with hand-written WebGL — no 3D library, no build-time asset
baking, no server. The repository stores only .gpx files; every statistic,
elevation profile and terrain surface is derived from them.
routes/*.gpx ← the only data in this repo
src/gpx.js ← parsing + track analysis (runs in the browser and in Node)
src/terrain.js ← mercator maths, tile loading, mesh + ribbon geometry
src/renderer.js ← WebGL programs, orbit camera, chase camera
src/viewer.js ← RouteViewer: the mountable component
src/library.js ← routes added in the browser (localStorage)
src/cards.js ← the month-by-month route list, shared by the list pages
src/share.js ← what a route's link is, and getting it onto the clipboard
src/theme.js ← light/dark, chosen rather than inherited
src/ambient.js ← the figures wandering across the landing page background
src/ui.css ← design tokens + viewer chrome + the page chrome
index.html ← the landing page: a live route, flying itself
routes.html ← your routes — the account's, plus this browser's
library.html ← the public library of sample routes
view.html ← single-route viewer (?route=|?local=|?cloud=)
r.html ← a shared link (/r/<token>)
scripts/build.mjs ← scans routes/, writes dist/ + routes.json
In the browser, for yourself. Hit + Add a GPX on Your routes, or drop files anywhere on that page. The track is parsed in the browser, added to the library and opened straight away — same pipeline as any other route: tiles for its bounding box, terrain mesh, ribbon, walker.
Those routes live in localStorage, private to that browser, and are labelled
this browser with an ✕ to remove them. They can't be anything else: Vercel
serves this site from a read-only filesystem, so an upload has nowhere on the
server to go — and the repository should stay the source of truth for what
everyone sees. There is no upload endpoint and no database.
In the repo, for everyone. Drop the .gpx into routes/, commit, push. The
build scans the folder, derives name, date, distance, ascent, bounding box and a
profile for each track, and writes routes.json beside the copied site. A card
appears for it, and it gets a standalone file of its own.
The date is the one the recorder wrote into the file. Plenty of GPX files carry no time at all — anything drawn by hand, or exported with the timestamps stripped — and those show the day the route joined the library instead, marked added, taken from the commit that introduced the file.
cp ~/Downloads/my-hike.gpx routes/
git add routes/my-hike.gpx && git commit -m "add my-hike" && git push
The hosted site is the product — the pages above, served from dist/. Three ways to look at it, in order of how close they are to production:
| how | needs | |
|---|---|---|
| Local dev | npm run dev → http://localhost:5173 | Node, nothing else |
| Production | push to GitHub, import in Vercel | GitHub + Vercel account |
| Single file | open dist/standalone/<slug>.html | just a browser |
You do not need GitHub or Vercel to work locally — npm run dev builds and
serves the real site on your machine, and that is exactly what Vercel serves. A
plain open index.html will not work: the viewer is ES modules, which browsers
refuse over file://. That is the only reason the standalone build exists.
A convenience artifact, not the main deliverable. npm run build also writes
dist/standalone/<slug>.html for every route: one self-contained file, no server
and no modules, that you can double-click or mail to someone. It still fetches imagery and elevation at view time (and degrades the
same way when offline), but the code, styles and track data are all inlined.
Every row in the public library links to its own.
That build is what scripts/build.mjs bundles by hand — each module wrapped in an
IIFE returning its exports, since browsers refuse ES modules over file://.
npm run dev # build + serve on http://localhost:5173
npm run build # write dist/ only
A static server is required — the viewer is ES modules, which browsers refuse to
load over file://.
vercel.json:
build command node scripts/build.mjs, output directory dist, no install
step (there are no dependencies).main redeploys automatically; pull requests get
their own preview URL..github/workflows/build.yml runs the same build on push and PR, so a malformed
GPX fails the check before it reaches production.
vercel.json serves /src, /routes, /standalone and routes.json with
max-age=0, must-revalidate. Those filenames are stable across deploys, so any
positive max-age lets a browser fetch the new HTML and keep running the previous
deploy's JavaScript until the cache expires — the site looks like it did not
update. Conditional requests are cheap; stale code is not. (And note the file is
strict JSON: Vercel's schema rejects unknown keys, so no "//" comments.)
At view time the browser fetches two tile sets covering the route's bounding box:
| Layer | Source | Zoom | Roughly |
|---|---|---|---|
| Imagery | Esri World Imagery (OpenStreetMap as fallback) | ≤16, ≤8×8 tiles | 2048² texture |
| Elevation | AWS Terrain Tiles (terrarium RGB encoding) | ≤14, ≤4×4 tiles | ~10 m per sample |
Those become a 352×352 vertex mesh (~247k triangles, four draw calls per frame) with the imagery draped over it as a single texture. The track is snapped to the rendered triangles rather than the raw elevation samples, and each ribbon edge takes the surface height beneath itself, so it drapes across cross-slopes instead of cutting into them.
It is a light workload — any GPU from the last decade holds 60 fps, phones included. The server does nothing but serve static files.
If the tile services are unreachable (offline, blocked, CORS), nothing breaks: imagery falls back to OpenStreetMap, then to a shaded elevation ramp, and if elevation data is unavailable too the terrain is modelled from the track's own recorded elevations. The status chip in the viewer always names what actually loaded.
The default view is oriented to the route itself: the camera sits on the start side looking down the track, so the route reads start-near → finish-far rather than at some arbitrary angle. (On a loop, where start and finish coincide, it aims at the farthest point on the track instead.)
course[], a
bearing taken from a look-ahead point a few hundred metres up the track, so
the camera follows where the route is going instead of yawing through every
switchback.sx/sy/sz), the cursor is a fractional index so she glides between
samples, and her facing eases towards the local bearing instead of snapping to
it.space play/pause · c chase camera · p recorded pace · r reset view.A 6" screen can't carry three permanent panels plus the thing they describe, so the chrome folds:
Imagery © Esri and its contributors. Elevation from AWS Terrain Tiles (SRTM, 3DEP and others). Map fallback © OpenStreetMap contributors.
Esri's basemaps are served here without an API key, which is fine for personal
and evaluation use but is not a production licence. For a public site, either
sign up for an ArcGIS developer key, switch imagery to a service you have
rights to (Mapbox, MapTiler), or pre-bake a single imagery snapshot per route and
serve that instead.