/* Studio 76 — Try It On. Upload your photo + a piece → we place the real piece on you, true to size, with a palette read from your skin. Talks only to blueprint.php. */ const PLACES = [ { k: 'neck', label: 'Neck' }, { k: 'ears', label: 'Ears' }, { k: 'hand', label: 'Hand' }, { k: 'wrist', label: 'Wrist' }, ]; function placeFor(type) { const t = (type || '').toLowerCase(); if (/ring/.test(t)) return 'hand'; if (/ear/.test(t)) return 'ears'; if (/bracelet|bangle|cuff|anklet/.test(t)) return 'wrist'; return 'neck'; // necklace / pendant / choker / default } async function api(action, payload) { const r = await fetch('blueprint.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action }, payload)) }); let j = {}; try { j = await r.json(); } catch (e) {} if (!r.ok || j.error) throw new Error(j.error || ('Something went wrong (' + r.status + ')')); return j; } function downscale(file, max) { return new Promise((res, rej) => { const fr = new FileReader(); fr.onerror = () => rej(new Error('read')); fr.onload = () => { const img = new Image(); img.onload = () => { const s = Math.min(1, max / Math.max(img.width, img.height)); const w = Math.round(img.width * s), h = Math.round(img.height * s); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(img, 0, 0, w, h); res(c.toDataURL('image/jpeg', 0.86)); }; img.onerror = () => rej(new Error('img')); img.src = fr.result; }; fr.readAsDataURL(file); }); } const isData = x => typeof x === 'string' && x.slice(0, 5) === 'data:'; const absUrl = u => { try { return new URL(u, location.href).href; } catch (e) { return u; } }; function Drop({ label, hint, onFile }) { const ref = React.useRef(null); const [over, setOver] = React.useState(false); return (
ref.current && ref.current.click()} onDragOver={e => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={e => { e.preventDefault(); setOver(false); onFile(e.dataTransfer.files[0]); }}>

{label} browse

{hint}

onFile(e.target.files[0])} />
); } function TryOn() { const [selfie, setSelfie] = React.useState(null); // dataUrl const [piece, setPiece] = React.useState(null); // dataUrl OR absolute url const [spec, setSpec] = React.useState(null); const [place, setPlace] = React.useState('neck'); const [adv, setAdv] = React.useState(null); // null | 'loading' | {undertone, metals, stones, note} const [out, setOut] = React.useState('idle'); // idle | loading | dataUrl const [cap, setCap] = React.useState(null); // null | 'loading' | {caption, hashtags} const [copied, setCopied] = React.useState(false); const [pieceUrlText, setPieceUrlText] = React.useState(''); const [err, setErr] = React.useState(''); // Arrive from a saved set (?set=), a direct image (?img=), or a piece handed off from the Stylist React.useEffect(() => { let handed = null; try { handed = sessionStorage.getItem('s76_piece'); if (handed) sessionStorage.removeItem('s76_piece'); } catch (e) {} if (handed) { setPiece(handed); setSpec(null); api('analyze', { image: handed }).then(a => { setSpec(a.spec); setPlace(placeFor(a.spec && a.spec.piece_type)); }).catch(() => {}); return; } const q = new URLSearchParams(location.search); const setId = (q.get('set') || '').replace(/[^a-f0-9]/g, ''); const img = q.get('img'); if (/^[a-f0-9]{20}$/.test(setId)) { const base = 'sets/' + setId + '/'; fetch(base + 'index.json').then(r => r.json()).then(m => { const cover = m.original ? base + m.original : (m.items && m.items[0] ? base + m.items[0].file : ''); if (cover) setPiece(absUrl(cover)); if (m.spec) { setSpec(m.spec); setPlace(placeFor(m.spec.piece_type)); } }).catch(() => {}); } else if (img) { bringPiece(img); } }, []); async function onSelfie(file) { if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; } setErr(''); setAdv('loading'); try { const d = await downscale(file, 1280); setSelfie(d); try { const a = await api('skintone', { selfie: d }); setAdv(a); } catch (e) { setAdv(null); } } catch (e) { setErr('Could not read that photo.'); setAdv(null); } } async function onPieceFile(file) { if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; } setErr(''); try { const d = await downscale(file, 1280); setPiece(d); setSpec(null); try { const a = await api('analyze', { image: d }); setSpec(a.spec); setPlace(placeFor(a.spec && a.spec.piece_type)); } catch (e) {} } catch (e) { setErr('Could not read that image.'); } } async function bringPiece(url) { url = (url || '').trim(); if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; } setErr(''); setPiece(url); setSpec(null); setPieceUrlText(''); try { const a = await api('analyze', { imageUrl: url }); setSpec(a.spec); setPlace(placeFor(a.spec && a.spec.piece_type)); } catch (e) {} } function tryOn() { if (!selfie || !piece || out === 'loading') return; setErr(''); setOut('loading'); setCap(null); setCopied(false); const payload = { selfie, place, spec: spec || {} }; if (isData(piece)) payload.piece = piece; else payload.pieceUrl = absUrl(piece); api('tryon', payload) .then(j => setOut(j.dataUrl)) .catch(e => { setOut('idle'); setErr(e.message || 'Try-on failed — try a clearer, front-facing photo.'); }); } function loadCaption() { setCap('loading'); setCopied(false); api('caption', { spec: spec || {} }).then(j => setCap(j)).catch(e => { setCap(null); setErr(e.message || 'Could not write a caption.'); }); } function capFull(c) { return (c.caption || '') + '\n\n' + (c.hashtags || []).map(h => '#' + String(h).replace(/^#/, '').replace(/\s+/g, '')).join(' '); } function copyCap(c) { const t = capFull(c); const done = () => { setCopied(true); setTimeout(() => setCopied(false), 1800); }; try { navigator.clipboard.writeText(t).then(done, done); } catch (e) { done(); } } const ready = selfie && piece; return (
Studio 76 BenchImaginariumStylist✦ Try It On

See it on you.

Add a clear, front-facing photo and the piece you love — we place the real piece on you, true to its actual size, and read a palette made for your skin.

{err &&

{err}

}
{/* left: inputs */}
1 · Your photo {selfie ?
You
: }
{adv === 'loading' &&
Reading your palette…
} {adv && adv !== 'loading' && (
Your palette · {adv.undertone} undertone
Metals that flatter you: {(adv.metals || []).map(m => {m})}
Stones to love: {(adv.stones || []).map(s => {s})}
{adv.note &&

“{adv.note}”

}
)}
2 · The piece {piece ?
Piece
:
setPieceUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') bringPiece(pieceUrlText); }} />
}
3 · Where to wear it
{PLACES.map(p => )}
{/* right: result */}
{out === 'loading' ?
Placing the piece on you…
: isData(out) ? You wearing the piece :
{ready ? 'Tap “Try it on me” to see it worn.' : 'Add your photo and a piece to begin.'}
}
{isData(out) && (
Download {!cap && }
{cap === 'loading' &&

Writing a caption…

} {cap && cap !== 'loading' && (
✦ Post to Instagram