/* Studio 76 Atelier — THE DRESSING ROOM (virtual stylist).
Your photo → a full-length virtual model (identity-locked) → trend chips or your own
words → styled looks, ONE at a time. Talks only to dressing.php (keys server-side). */
const STYLES = [
'Quiet luxury', 'Old money', 'Power tailoring', 'Soft minimal', 'Streetwear',
'Athleisure', 'Indo-western', 'Festive', 'After dark', 'Coastal linen',
'Denim on denim', 'Monochrome', 'Colour pop', 'Vintage revival',
];
const MKEY = 's76_dr_model';
function downscale(file, max) {
return new Promise((res, rej) => {
const fr = new FileReader();
fr.onerror = () => rej(new Error('Could not read that file.'));
fr.onload = () => {
const img = new Image();
img.onload = () => {
const s = Math.min(1, max / Math.max(img.width, img.height));
const c = document.createElement('canvas');
c.width = Math.round(img.width * s); c.height = Math.round(img.height * s);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
res(c.toDataURL('image/jpeg', 0.85));
};
img.onerror = () => rej(new Error('That does not look like an image.'));
img.src = fr.result;
};
fr.readAsDataURL(file);
});
}
async function api(action, payload) {
const r = await fetch('dressing.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({ action }, payload)),
});
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j.error || ('Server error ' + r.status));
return j;
}
function LookCard({ look }) {
if (!look) return null;
return (
{look.name}
{(look.pieces || []).map((p, i) => (
- {p.item}{p.why ? — {p.why} : null}
))}
{look.palette ? Palette{look.palette} : null}
{look.shoes ? Shoes{look.shoes} : null}
{look.accents ? Accents{look.accents} : null}
{look.note ?
{look.note}
: null}
);
}
function DressingRoom() {
const [stage, setStage] = React.useState('capture'); // capture -> model -> room
const [photo, setPhoto] = React.useState(null);
const [model, setModel] = React.useState(null); // {id, dataUrl}
const [styles, setStyles] = React.useState([]);
const [custom, setCustom] = React.useState('');
const [closet, setCloset] = React.useState('');
const [look, setLook] = React.useState(null); // {look, dataUrl}
const [seen, setSeen] = React.useState([]);
const [tweak, setTweak] = React.useState('');
const [busy, setBusy] = React.useState('');
const [err, setErr] = React.useState('');
const [saved, setSaved] = React.useState(null);
const fileRef = React.useRef(null);
const camRef = React.useRef(null);
React.useEffect(() => {
try {
const m = JSON.parse(localStorage.getItem(MKEY) || 'null');
if (m && m.dataUrl) { setModel(m); setStage('model'); }
} catch (e) {}
}, []);
const fail = (e) => { setErr(e.message || String(e)); setBusy(''); };
async function onFile(file) {
if (!file) return;
setErr('');
try { setPhoto(await downscale(file, 1280)); } catch (e) { fail(e); }
}
async function makeModel() {
if (!photo) return;
setBusy('Sketching your model — 20 seconds or so…'); setErr('');
try {
const r = await api('model', { photo });
const m = { id: r.id, dataUrl: r.dataUrl };
setModel(m); setStage('model'); setBusy('');
try { localStorage.setItem(MKEY, JSON.stringify(m)); } catch (e) {}
} catch (e) { fail(e); }
}
async function dressMe(surprise) {
if (!model) return;
setBusy(surprise ? 'The stylist is choosing for you…' : 'Styling your look…'); setErr('');
try {
const r = await api('outfit', {
model_id: model.id, model: model.dataUrl, styles, custom, closet,
avoid: seen, surprise: !!surprise,
});
setLook(r); setStage('room'); setBusy(''); setTweak(''); setSaved(null);
if (r.look && r.look.name) setSeen((s) => [...s.slice(-7), r.look.name]);
} catch (e) { fail(e); }
}
async function doTweak() {
if (!look || !tweak.trim()) return;
setBusy('Adjusting…'); setErr('');
try {
const r = await api('refine', { image: look.dataUrl, instructions: tweak.trim(), aspect: '3:4' });
setLook((l) => ({ ...l, dataUrl: r.dataUrl })); setBusy(''); setTweak('');
} catch (e) { fail(e); }
}
async function saveLook() {
if (!look) return;
setBusy('Saving to your lookbook…'); setErr('');
try {
const s = await api('save_start', { spec: { garment_type: (look.look && look.look.name) || 'Styled look' } });
await api('save_item', { setid: s.setid, key: 'look', label: (look.look && look.look.name) || 'The look', dataUrl: look.dataUrl });
await api('save_item', { setid: s.setid, key: 'model', label: 'The model', dataUrl: model.dataUrl });
setSaved(s.url); setBusy('');
} catch (e) { fail(e); }
}
function retake() {
setModel(null); setPhoto(null); setLook(null); setSeen([]); setStage('capture');
try { localStorage.removeItem(MKEY); } catch (e) {}
}
const chip = (s) => (
);
return (
The Dressing Room
Your stylist, who has actually met you.
One photo becomes your virtual model. Then the room dresses you: one considered look at a time, never a lineup.
{err ?
{err}
: null}
{busy ?
{busy}
: null}
{stage === 'capture' && (
fileRef.current && fileRef.current.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => { e.preventDefault(); onFile(e.dataTransfer.files && e.dataTransfer.files[0]); }}>
{photo
?

:
Drop your photo here
or tap to choose · we keep nothing
}
onFile(e.target.files[0])} />
onFile(e.target.files[0])} />
)}
{stage === 'model' && model && (
Your model · ready to be styled
The style rail
{STYLES.map(chip)}
In your words optional
setCustom(e.target.value)}
placeholder="date night but comfortable · a wedding in Jaipur · first day at a new job" />
Your closet optional
)}
{stage === 'room' && look && (
{(look.look && look.look.name) || 'The look'}
setTweak(e.target.value)}
placeholder="make the trousers wide-leg · swap to gold accents · softer colours" />
{saved ?
Saved — your look, shareable
: null}
)}
);
}
ReactDOM.createRoot(document.getElementById('root')).render();