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
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.
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}))
| Sección | Descripción |
|---|---|
| Navbar | <ModernNavbar client:load /> (componente React del landing, compartido) |
| Hero grid 40/60 | lg: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) |
| FAQ | 5 items <details>: qué es Spin View, privacidad, WhatsApp/EXIF, Android/iPhone, formatos |
| Footer | <ModernFooter client:visible /> (componente React del landing, compartido) |
Todas dentro de un IIFE vanilla (~250 líneas). Sin librerías.
| Función | Rol |
|---|---|
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 |
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).
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.
NATIVE_W = 4032, NATIVE_H = 3024aspect-ratio 9/16 (historia Instagram), móvil 16/10 con max-height 38vh (franja compacta, no torre).has-img .mgs-drop-copy { display: none })hidden), reemplazado por panel EXIF + opciones + botón[hidden]{display:none !important} : regla crítica — las clases Tailwind flex pisaban el atributo hidden nativo#141A22, valores nuevos en tan #DDA15E, valores viejos tachados.mgs-* (nunca #mgs-tool — un ID en el mismo elemento con clase grid de Tailwind pisaba display:grid por especificidad y rompía el layout):root (--ink, --panel, --line, --soft, --paper, --wash, --signal:#606C38) — si viven en un wrapper opcional, caen a transparente.mgs-tool input[...] y la card había perdido la clase → siempre default “native”. Fix: clase mgs-tool en la card derechastyle="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)].
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).
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.
hidden nativo vs clases Tailwind: [hidden] nativo pierde contra .flex { display:flex } (ambas declaraciones al mismo nivel). Fix global: [hidden]{display:none !important}.
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.
Instagram redimensiona por ancho: salida vertical genera zoom. Siempre landscape (las lentes reales graban landscape + EXIF Orientation endereza).
global.css del landing)<ModernNavbar> / <ModernFooter> React components del landing (client:load / client:visible)Flujo estándar Digitalizatodo: git push origin main → Coolify deploy app landing (UUID tgkcc4k4484wswwcc8g884o4). Ver AGENTS.md > Deploy.
Commits históricos:
fd0f3ba3 feat(landing): add efecto-spin-meta EXIF stamp tool300e10e5 fix(landing): landscape 4:3 output + mobile dropzone 4:57a6ba864 fix(landing): radio selector broken, output size always native278fee11 fix(landing): iOS share sheet for spin-meta download