@pykero/eyewear-vto
v0.1.0
Published
Browser-based virtual try-on widget for eyewear, rendered at true physical size.
Maintainers
Readme
@pykero/eyewear-vto
Browser-based virtual try-on for eyewear. Frames render at true physical size on the shopper's face, from a webcam, entirely on-device.
Status: 0.1.0 — not shippable to shoppers. Every phase is implemented and unit-tested. No phase gate has been verified, because verification requires recorded fixtures that do not exist yet and a human looking at a camera. See ADR-0012 and docs/phase-status.md. The API is stable enough to build against; the numbers it draws are not yet trustworthy.
Live demo: https://pykero-tryon.pages.dev — open on a phone, tap Start.
Install
npm install @pykero/eyewear-vto@preview three @mediapipe/tasks-vision@preview is required — nothing is published to latest until a phase gate is
verified. three and @mediapipe/tasks-vision are peer dependencies so a host
app that already uses three.js does not ship a second copy of it. Changes per
version: CHANGELOG.md.
Full integration guide: docs/integration.md — your own frames, self-hosted tracker assets, events, privacy.
How it works: docs/how-it-works.md — the runtime, end to end: the two clocks, the tracker boundary, occlusion, metric scale, lighting.
Use — module
import '@pykero/eyewear-vto' // registers <eyewear-tryon><eyewear-tryon sku="DEV-58"></eyewear-tryon>Use — plain HTML
<script src="https://cdn.jsdelivr.net/npm/@pykero/[email protected]/dist-lib/eyewear-vto.iife.js"></script>
<eyewear-tryon sku="DEV-58"></eyewear-tryon>That is the IIFE build (@pykero/eyewear-vto/iife), which bundles three.js for pages
with no module resolver; jsDelivr serves it straight from the npm package. See
demo.html for the same thing against a local build.
API
| | |
|---|---|
| start() | Loads the tracker (~5 MB) and opens the camera. Never runs on page load. |
| stop() | Releases the camera and tears down the scene. |
| sku | Get/set the current SKU. Also settable as an attribute. |
| setFrames(frames) | Replace the catalogue. Bare FrameSpecs render procedurally; SkuEntrys carry a glTF modelUrl. Mixed lists are fine. |
| assetUrls | Serve the MediaPipe runtime and model from your own origin. Also wasm-base / model-url attributes. |
| calibrate() | Show the ID-1 card guide. |
| measure() | Take the measurement. Returns PD in mm, or null if rejected. |
| clearCalibration() | Delete the stored measurement from this device. |
| pdMm | Card-referenced PD, or null when uncalibrated. |
| capture(type?) | A still of the try-on as a data URL. |
Events, all CustomEvent and composed: vto-ready, vto-error,
vto-tracking-changed, vto-calibrated, vto-calibration-rejected,
vto-model-rejected, vto-frame-changed, vto-capture.
Your own frames
el.setFrames([
{ spec: { sku: 'ACME-52', name: 'Acme Round', lensWidthMm: 52, bridgeMm: 18,
templeLengthMm: 145, lensHeightMm: 42, rimThicknessMm: 3,
frontWidthMm: 138 },
modelUrl: '/frames/acme-52.glb' },
])The millimetres are the render, not metadata. A glTF is measured against them on
load and refused if it disagrees — never silently rescaled, because a
wrongly-scaled model is internally consistent and would otherwise be
undetectable. Failures fall back to the procedural frame and fire
vto-model-rejected.
Style the control bar with ::part(controls). Everything else is in a shadow
root and closed to host CSS on purpose — product pages have opinionated resets.
Privacy
No video frame, landmark, or derived measurement ever leaves the browser. No
network call in this package carries anything derived from the camera. The only
thing persisted is the calibration — six numbers in localStorage: head-size
ratio, PD, distance, a timestamp and the capture resolution. No image, no
landmarks, no embedding, and nothing that identifies a person. clearCalibration()
removes it.
capture() returns a data URL to you. This package never uploads it. If your
page does, that is your decision to disclose.
Requirements
- HTTPS, or
localhost.getUserMediarequires a secure context. - WebGL2 for lens refraction. Without it the lens degrades to a tinted surface.
- The MediaPipe WASM runtime and model (~5 MB) are fetched on first
start(). Defaults are public CDNs; setwasm-baseandmodel-urlto serve them from your own origin, which you should do in production.
Publishing
Releases are published by CI, not from a laptop — see
publish.yml and
ADR-0023.
Pushing a v* tag builds, checks the tarball, publishes to npm under the
preview dist-tag, then installs the published package into a fresh
Vite app and a plain HTML page (IIFE from jsDelivr) and asserts the element
renders in headless Chrome.
# 1. bump: package.json "version", VERSION in src/widget/embed/index.ts, CHANGELOG.md
# 2. merge to main, then
git tag v0.1.0 && git push origin v0.1.0The workflow refuses a tag whose version does not match all three. It needs an
NPM_TOKEN repository secret (granular token, publish scope, bypass 2FA).
To dry-run the consumer side locally against an unpublished build:
npm run build:lib && npm pack --ignore-scripts
tools/smoke-registry.sh ./pykero-eyewear-vto-0.1.0.tgz dist-lib/eyewear-vto.iife.jsWhy not from a laptop: if ignore-scripts=true is set in your npm config —
a common security hardening, and easy to forget — prepack is silently skipped
and you publish a package containing nothing but README.md and
package.json. It does not error. The workflow builds explicitly and inspects
the tarball instead of trusting the hook.
Development
npm run dev # harness at localhost:5173 (or next free port)
npm test # unit tests
npm run build:all # app + ESM + IIFE + declarations
npm run size # gzipped size vs budget (run build:all first)npm run size enforces the §10 cap of 5 MB gzipped, plus a per-bundle budget for
the ESM build, the IIFE build and the harness entry chunk. CI runs it after both
builds and fails on a breach. Raising a budget is a one-line edit to
tools/bundle-budget.json — AGENTS.md §8 asks only that
the PR say what the extra weight bought.
One thing the number does not include: MediaPipe's ~5 MB WASM runtime and model
are fetched from a CDN on the shopper's click and never appear in dist/. The
budget measures what this repo ships, not everything a shopper downloads.
Read AGENTS.md before changing anything. It carries ten invariants that are build-breaking rules, not suggestions — the first of which is that frames are never scaled to fit a detected face.
