Efecto Spin View Meta — Documentación

Herramienta web que marca fotos como capturadas por Ray-Ban Meta Smart Glasses 2 reescribiendo su EXIF, para que Instagram active el efecto Spin View (inclinación 3D al mover el celular).

URL producción: https://digitalizatodo.cl/efecto-spin-meta Archivo fuente: Applications/landing/src/pages/efecto-spin-meta.astro


Contexto

Instagram activa el efecto Spin View cuando reconoce que una foto viene de unos lentes Ray-Ban Meta. Las Ray-Ban Meta reales registran en el EXIF de la foto una identidad de cámara (Make = "Meta AI", Model = "Ray-Ban Meta Smart Glasses 2"), junto con dimensiones nativas 4032×3024 (4:3 horizontal). Esta herramienta inyecta esa identidad en fotos cualesquiera, 100% en el navegador.


Arquitectura

Página Astro standalone en /efecto-spin-meta. No hay backend — todo el procesamiento es client-side (HTML + CSS inline + un solo <script is:inline> vanilla).

Usuario sube foto
  ↓
[load()]  file.arrayBuffer() → readExif() lee Make/Model/Orientation originales
  ↓
[decode()] createImageBitmap({ imageOrientation: "from-image" }) → fallback decodeViaImg()
  ↓
Usuario elige medida de salida (4032×3024 nativo | Original)
  ↓
[run()]  canvasToBytes() (reencodeo JPEG 0.96) o bytes originales si "untouched"
  ↓
[stamp()]  Elimina APPn/COM markers del JPEG original → inyecta APP1 EXIF nuevo
  ↓
Descarga (desktop/Android: <a download> / iOS: navigator.share({files}))

Componentes de la página

SecciónDescripción
Navbar<ModernNavbar client:load /> (componente React del landing, compartido)
Hero grid 40/60lg:grid-cols-[2fr_3fr] — izquierda card negra con headline + features, derecha tool
Card izquierda (negra #0A0A0A)Eyebrow tan “Herramienta gratuita”, headline blanco, 3 feature cards (bg-white/5 border-white/10)
Card derecha (grid interno 3 cols)Dropzone (1 col) | resultado/panel/opciones (2 cols, lg:col-span-2)
FAQ5 items <details>: qué es Spin View, privacidad, WhatsApp/EXIF, Android/iPhone, formatos
Footer<ModernFooter client:visible /> (componente React del landing, compartido)

Funciones JS clave

Todas dentro de un IIFE vanilla (~250 líneas). Sin librerías.

FunciónRol
readExif(buf)Lee DataView del JPEG, busca marker 0xE1 (APP1 “Exif\0\0”), retorna { make, model, orientation }
parseTiff(d, base, out)Parsea el TIFF interno: tags 0x010F (Make), 0x0110 (Model), 0x0112 (Orientation)
buildApp1()Construye segmento APP1 completo: TIFF header (MM big-endian) + IFD con entries — Make, Model, Orientation=1, X/YResolution=72, ResolutionUnit=2, YCbCrPositioning=1
stamp(bytes)Recorre markers JPEG del original, elimina todos los APPn/COM (borra EXIF viejo y datos de ubicación), inyecta el APP1 nuevo después del SOI (0xFFD8)
toNative(bitmap)Canvas 4032×3024, escala cover + centrado (crop 4:3 horizontal)
toSameSize(bitmap)Canvas al tamaño original de la foto
canvasToBytes(canvas)toBlob("image/jpeg", 0.96) → Uint8Array
decode(file)createImageBitmap con orientación aplicada; fallback a <img>
load(file) / run()Flujo principal + descarga

Descarga multiplataforma (critical)

Safari iOS no soporta <a download>. El flujo en run():

if (iOS && navigator.canShare({files}))  →  navigator.share({ files: [blob] })  →  "Guardar imagen" en Fotos
else                                     →  fallbackDownload() (a.download clásico)
AbortError (usuario cancela share)       →  silencioso, revoca URL

Detección iOS: /iPad|iPhone|iPod/.test(ua) || (platform === "MacIntel" && maxTouchPoints > 1) (iPadOS 13+ se hace pasar por Mac).


Formato de salida — POR QUÉ 4:3 HORIZONTAL

Lección aprendida (importante): la primera versión usaba 3024×4032 vertical (3:4). Resultado: Instagram redimensiona usando el ancho como base — con la imagen vertical el ancho es “corto”, la plataforma escala hasta llenar el viewer, el alto se desborda → zoom artificial + efecto widescreen al voltear el celular.

Corrección: las Ray-Ban Meta reales capturan en landscape 4032×3024 (4:3) con EXIF Orientation que la endereza. Ese ancho grande ya es la “base” que Instagram respeta → sin zoom, sin vacíos.


UX / decisiones de UI


Pipa al celular sin perder EXIF


Bugs encontrados durante el desarrollo (lecciones)

  1. style="paddingTop: 'clamp(...)'" en .astro: Astro lo pasa literal como string CSS → inválido, el navegador lo ignora. En React (style={{}}) sí funciona. Solución: clases Tailwind arbitrarias pt-[clamp(6rem,14vh,9rem)].

  2. Tailwind classes faltantes con cdn.tailwindcss.com: las clases arbitrarias min-h-[320px] necesitan el CDI de Tailwind 3.4.17 para funcionar (no la primera vez que se renderiza).

  3. Selector ID vs clase Tailwind: #mgs-tool { display:block } (especificidad ID) pisa .grid { display:grid } (clase). Solución: mover CSS del tool a clases puras .mgs-* y variables a :root.

  4. hidden nativo vs clases Tailwind: [hidden] nativo pierde contra .flex { display:flex } (ambas declaraciones al mismo nivel). Fix global: [hidden]{display:none !important}.

  5. Radios no funcionan: si el query del JS no matchea el contenedor (cambió de id/div), chosenSize() cae al default silenciosamente. Confirmar con el panel “Medidas” que reacciona al cambiar el radio.

  6. Instagram redimensiona por ancho: salida vertical genera zoom. Siempre landscape (las lentes reales graban landscape + EXIF Orientation endereza).


Verificación manual

  1. Soltar un JPEG cuadrado (ej. 460×460) → panel “Medidas” debe mostrar 460 × 460
  2. Radio en “4032 × 3024” (default) → descargar → archivo 4032×3024
  3. Cambiar radio a “Original” → panel “Medidas” sigue 460 × 460 → descargar → archivo cuadrado sin recorte
  4. En iPhone: “Marcar y descargar” → sheet nativo iOS → “Guardar imagen” → EXIF verificado en app Fotos (swipe up muestra “Meta AI Ray-Ban Meta…”)
  5. Subir a Instagram → efecto Spin View animado al tocar la imagen

Stack

Deploy

Flujo estándar Digitalizatodo: git push origin main → Coolify deploy app landing (UUID tgkcc4k4484wswwcc8g884o4). Ver AGENTS.md > Deploy.

Commits históricos: