Back to Projects
Meta Gyro Tilt
CompletedJavaScriptCanvas 2DEXIF+2 more

Meta Gyro Tilt

A 100% local, zero-backend image normalizer that turns any JPG into a 3024×4032 Ray-Ban Meta frame. Corrects gyro tilt via Canvas, strips private EXIF and stamps Meta tags without ever uploading.

Timeline

August 2026

Role

Creator & Developer

Status
Completed

Technology Stack

JavaScript
Canvas 2D
EXIF
PWA
Cloudflare Workers

Overview

Meta Gyro Tilt is a single-file, zero-backend Format Press that fakes a Ray-Ban Meta POV directly in the browser.

Drop any JPG, it corrects EXIF Orientation (1 to 8), applies an optional ±15° tilt, resizes to 3024×4032 (or 1080×1920 for Story), wipes GPS and private tags, stamps Make: Meta AI / Model: Ray-Ban Meta Smart Glasses 2 and exports a share-ready JPEG + pure Base64. No server, no upload.

Live at gyro.10akhil-t.workers.dev and open source at github.com/silky-x0/meta-gyro-tilt. For testing and education only, unofficial and not affiliated with Meta.

Meta Gyro Tilt flow

Key Capabilities

  • Gyro Correction: Reads EXIF Orientation with piexifjs and maps all 8 values to Canvas 2D setTransform(), then composes manual tilt with translate -> rotate -> translate.
  • Story-Ready Resize: cover (center crop via Math.max(dw/iw, dh/ih)), contain and stretch modes. Exports at 95% JPEG via canvas.toBlob for lower peak memory than toDataURL.
  • Privacy First: exif.GPS = {} and strips Software / MakerNote / Lens* before export. No fetch with image bytes, images never cached.
  • Deterministic Output: Same input + same fit/tilt/preset produces same bytes and PixelXDimension / PixelYDimension, useful for QA pipelines that validate Make/Model.
  • Share to Story: Copies Base64 via navigator.clipboard with execCommand fallback, and shares via navigator.share({files}) which surfaces Instagram -> Story on iOS/Android. Falls back to URL.createObjectURL download.
  • PWA Offline: manifest.json (display: standalone) + sw.js (stale-while-revalidate for CDN only) makes it installable and usable offline after first load. SRI hash + onerror fallback for piexif.js.

How It Works

1. Input & Preview (The Loader) Validates image/jpeg and .jpg/.jpeg extension, guards with a 15 MB limit, and previews via FileReader.readAsDataURL. Three entry points (fileInput, cameraInput with capture="environment", drag-and-drop + keyboard Enter/Space) all funnel to handleFile().

2. Correction & Stamping (The Darkroom)

// read orientation
const orientation = piexif.load(dataUrl)["0th"][piexif.ImageIFD.Orientation] || 1

// draw corrected - orientation first, then manual tilt
switch (orientation) {
  case 6: ctx.setTransform(0, 1, -1, 0, targetH, 0); break; // 90° CW
  case 3: ctx.setTransform(-1, 0, 0, -1, targetW, targetH); break; // 180°
  // ... 1,2,4,5,7,8
}
if (tiltDeg) {
  ctx.translate(canvas.width/2, canvas.height/2);
  ctx.rotate(tiltDeg * Math.PI / 180);
  ctx.translate(-canvas.width/2, -canvas.height/2);
}
const blob = await new Promise(r => canvas.toBlob(r, "image/jpeg", 0.95));

// rebuild EXIF - wipe private, stamp Meta
exif.GPS = {};
delete exif["0th"][piexif.ImageIFD.Software];
exif["0th"][piexif.ImageIFD.Make] = "Meta AI";
exif["0th"][piexif.ImageIFD.Model] = "Ray-Ban Meta Smart Glasses 2";
exif["0th"][piexif.ImageIFD.Orientation] = 1;
exif.Exif[piexif.ExifIFD.PixelXDimension] = 3024;

Stamped with piexif.dump() + piexif.insert() to produce both data:image/jpeg;base64,... and pure Base64.

3. Export & Share (The Shipper) Filename adapts to preset (meta-glasses-3024x4032.jpg vs meta-story-1080x1920.jpg). Includes safe-area overlay (top/bottom 18%) so you can see where Instagram UI will cover the Story.

Local vs Server Mental Model

Server converters require uploading 8 MB JPGs and trusting a stranger's darkroom. This keeps everything in-memory: FileReader -> Canvas -> piexif. No queue, no leak, no bill. sw.js only caches the app shell and fonts, never images.

Trade-Offs

  • JPEG only by design, PNG/WebP renamed to .jpg fails at piexif.load
  • Single image at a time, 15 MB cap due to 2x memory for toBlob + Base64
  • ICC/XMP profiles dropped on re-encode, fine for Stories not print
  • cover is center-only, no drag or face-aware crop, tilt can clip corners at ±15°
  • First load needs CDN (cdnjs + Google Fonts), mitigated with SRI and fallback

What I Learned

EXIF is not metadata trivia, it is the image. Canvas is a state machine where setTransform order matters. toBlob beats toDataURL for 12 MP photos. And exif.GPS = {} is a one-liner that actually protects privacy.

Read the full build breakdown on the blog: /blog/meta-gyro and try it at gyro.10akhil-t.workers.dev.