/* Studio 76 Atelier — Apparel Bench. Upload a garment photo (or a sketch) → AI spec,
technical flat / tech-pack, front·back·detail plate, coloured illustration, and "see it worn"
on a model (Instagram-ready), plus variations, reforms and guided edits.
Talks only to apparel.php (keys server-side). */
const WEAR = [
{ k: 'full', label: 'Full length' },
{ k: 'runway', label: 'Runway' },
{ k: 'half', label: 'Waist up' },
{ k: 'back', label: 'Back view' },
{ k: 'seated', label: 'Seated' },
{ k: 'detail', label: 'Fabric detail' },
{ k: 'street', label: 'Street style' },
];
const MODELS = [
{ k: 'woman', label: 'On her' },
{ k: 'man', label: 'On him' },
];
const LOOKS = [
{ k: 'editorial', label: 'Editorial' },
{ k: 'indian', label: 'Indian' },
{ k: 'millennial', label: 'Millennial' },
{ k: 'glam', label: 'Glam' },
{ k: 'studio', label: 'Studio' },
{ k: 'goldenhour', label: 'Golden hour' },
];
const FORMS = [
{ k: 'dress', label: 'As a dress' },
{ k: 'top', label: 'As a top' },
{ k: 'skirt', label: 'As a skirt' },
{ k: 'trousers', label: 'As trousers' },
{ k: 'jacket', label: 'As a jacket' },
{ k: 'jumpsuit', label: 'As a jumpsuit' },
];
/* Guided-edit vocab — discrete controls, each = one precise change on the zoomed render. */
const NECKLINES = ['Crew', 'V-neck', 'Square', 'Sweetheart', 'Halter', 'Boat', 'Off-shoulder', 'Cowl', 'Collared'];
const SLEEVES = ['Sleeveless', 'Cap', 'Short', 'Three-quarter', 'Long', 'Puff', 'Bishop', 'Bell'];
const FABRICS = ['Silk', 'Satin', 'Cotton', 'Linen', 'Velvet', 'Chiffon', 'Crepe', 'Denim', 'Wool', 'Tulle'];
const FITS = ['Slim', 'Tailored', 'Relaxed', 'Oversized'];
const COLORS = [
{ name: 'Ivory', hex: '#F3EEE3' }, { name: 'Black', hex: '#1B130D' }, { name: 'Emerald', hex: '#0F6B4F' },
{ name: 'Ruby red', hex: '#8E1B2E' }, { name: 'Blush', hex: '#E7C3C0' }, { name: 'Royal blue', hex: '#27408B' },
{ name: 'Sapphire navy', hex: '#1C2B4A' }, { name: 'Champagne', hex: '#E4D2A8' }, { name: 'Sage', hex: '#A7B79A' },
{ name: 'Terracotta', hex: '#C2552B' },
];
const DETAILS = [
{ k: 'belt', op: 'add', params: { element: 'a slim matching waist belt' }, label: '+ Belt' },
{ k: 'pockets', op: 'add', params: { element: 'discreet side-seam pockets' }, label: '+ Pockets' },
{ k: 'ruffle', op: 'add', params: { element: 'a soft ruffle along the hem' }, label: '+ Ruffle hem' },
{ k: 'sleeveless', op: 'remove', params: { element: 'the sleeves' }, label: '– Sleeves' },
];
const SEL0 = { length: null, neckline: null, sleeve: null, fabric: null, color: null, fit: null, details: [], note: '' };
function cap1(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; }
/* aspect of a generated key, for the refine (perfect-it) pass */
function aspectFor(key) {
if (key === 'views') return '3:2';
if (key.slice(0, 4) === 'mag_') return '3:4';
if (key.slice(0, 2) === 'w_') return '4:5';
return '3:4';
}
async function benchApi(action, payload) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 95000);
let r;
try {
r = await fetch('apparel.php', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({ action }, payload)), signal: ctrl.signal });
} catch (e) {
clearTimeout(timer);
throw new Error(e && e.name === 'AbortError' ? 'That took too long — the studio is busy. Please try again.' : 'Network hiccup — please try again.');
}
clearTimeout(timer);
let j = null; try { j = await r.json(); } catch (e) {}
if (!r.ok || !j || j.error) throw new Error((j && j.error) || ('The studio is busy right now (' + r.status + ') — please try again.'));
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.85));
};
img.onerror = () => rej(new Error('img'));
img.src = fr.result;
};
fr.readAsDataURL(file);
});
}
function isUrl(x) { return typeof x === 'string' && x.slice(0, 5) === 'data:'; }
function Bench() {
const [stage, setStage] = React.useState('idle');
const [entry, setEntry] = React.useState('piece');
const [src, setSrc] = React.useState(null);
const [id, setId] = React.useState(null);
const [spec, setSpec] = React.useState(null);
const [out, setOut] = React.useState({});
const [look, setLook] = React.useState('editorial');
const [model, setModel] = React.useState('woman');
const [shots, setShots] = React.useState([]);
const [morphs, setMorphs] = React.useState([]);
const [fixes, setFixes] = React.useState([]);
const [fixN, setFixN] = React.useState(0);
const [varN, setVarN] = React.useState(0);
const [varText, setVarText] = React.useState('');
const [saving, setSaving] = React.useState(false);
const [savedN, setSavedN] = React.useState('');
const [savedUrl, setSavedUrl] = React.useState('');
const [drag, setDrag] = React.useState(false);
const [err, setErr] = React.useState('');
const [urlText, setUrlText] = React.useState('');
const [modalSrc, setModalSrc] = React.useState('');
const [modalKey, setModalKey] = React.useState('');
const [sel, setSel] = React.useState(SEL0);
const [cap, setCap] = React.useState(null);
const [copied, setCopied] = React.useState(false);
const fileRef = React.useRef(null);
const savedSetRef = React.useRef(null);
const savingRef = React.useRef(false);
const aspectsRef = React.useRef({});
async function ingestRealize(srcPayload) {
const a1 = await benchApi('analyze', srcPayload);
if (!a1.id) throw new Error('Could not read that sketch — please try again.');
const r = await benchApi('realize', { id: a1.id });
setSrc(r.dataUrl);
const a2 = await benchApi('analyze', { image: r.dataUrl });
if (!a2.id) throw new Error('Could not finish reading — please try again.');
setId(a2.id); setSpec(a2.spec);
}
function resetForIngest(sketch) {
setErr(''); setEntry(sketch ? 'sketch' : 'piece'); setStage('analyzing');
setOut({}); setSpec(null); setId(null); setShots([]); setMorphs([]); setFixes([]); setVarN(0); setSavedUrl(''); savedSetRef.current = null;
}
async function handleFile(file, sketch) {
if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; }
resetForIngest(sketch);
try {
const dataUrl = await downscale(file, 1280);
setSrc(dataUrl);
if (sketch) { await ingestRealize({ image: dataUrl }); }
else { const j = await benchApi('analyze', { image: dataUrl }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); }
setStage('ready');
} catch (e) { setErr(e.message || 'Could not read that image.'); setStage('idle'); }
}
async function handleUrl(url, sketch) {
url = (url || '').trim();
if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; }
resetForIngest(sketch);
setSrc(url);
try {
if (sketch) { await ingestRealize({ imageUrl: url }); }
else { const j = await benchApi('analyze', { imageUrl: url }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); }
setStage('ready');
} catch (e) { setErr(e.message || 'Could not bring that image in.'); setStage('idle'); }
}
React.useEffect(() => {
const u = new URLSearchParams(window.location.search).get('img');
if (u) { setUrlText(u); handleUrl(u); }
}, []);
async function gen(key, action, payload) {
if (!id) return;
setOut(o => Object.assign({}, o, { [key]: 'loading' }));
try { const j = await benchApi(action, Object.assign({ id }, payload)); setOut(o => Object.assign({}, o, { [key]: j.dataUrl })); }
catch (e) { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Generation failed.'); }
}
function genWear(place) {
const w = WEAR.find(x => x.k === place), l = LOOKS.find(x => x.k === look);
const key = 'w_' + place + '_' + look + '_' + model;
const lookLabel = l.label + (model === 'man' ? ' · Him' : '');
if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: w.label, lookLabel }].concat(prev));
gen(key, 'wear', { place, look, model });
}
function genMagazine() {
const l = LOOKS.find(x => x.k === look);
const key = 'mag_' + look + '_' + model;
const lookLabel = l.label + (model === 'man' ? ' · Him' : '');
if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: 'Magazine cover', lookLabel }].concat(prev));
gen(key, 'magazine', { look, model });
}
function genEditSet(ops, label) {
const srcKey = modalKey, base = out[srcKey];
if (!id || !srcKey || !isUrl(base) || !ops.length) return;
const aspect = aspectsRef.current[srcKey] || aspectFor(srcKey);
const n = fixN + 1; setFixN(n);
const key = 'fix_' + n;
aspectsRef.current[key] = aspect;
setFixes(prev => [{ key, label }].concat(prev));
closeModal();
setOut(o => Object.assign({}, o, { [key]: 'loading' }));
benchApi('edit', { id, image: base, ops, aspect })
.then(j => setOut(o => Object.assign({}, o, { [key]: j.dataUrl })))
.catch(e => { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Edit failed.'); });
}
function applyEdits() {
const ops = [], parts = [];
if (sel.length) { ops.push({ op: 'length', params: { dir: sel.length } }); parts.push(sel.length === 'longer' ? 'Longer' : 'Shorter'); }
if (sel.neckline) { ops.push({ op: 'neckline', params: { to: sel.neckline } }); parts.push(cap1(sel.neckline) + ' neck'); }
if (sel.sleeve) { ops.push({ op: 'sleeve', params: { to: sel.sleeve } }); parts.push(cap1(sel.sleeve) + ' sleeve'); }
if (sel.fabric) { ops.push({ op: 'fabric', params: { to: sel.fabric } }); parts.push(cap1(sel.fabric)); }
if (sel.color) { ops.push({ op: 'color', params: { to: sel.color } }); parts.push(cap1(sel.color)); }
if (sel.fit) { ops.push({ op: 'fit', params: { to: sel.fit } }); parts.push(cap1(sel.fit) + ' fit'); }
sel.details.forEach(k => { const d = DETAILS.find(x => x.k === k); if (d) { ops.push({ op: d.op, params: d.params }); parts.push(d.label); } });
const note = sel.note.trim();
if (note) { ops.push({ op: 'note', params: { text: note } }); parts.push('“' + (note.length > 24 ? note.slice(0, 24) + '…' : note) + '”'); }
if (!ops.length) return;
let label = parts.slice(0, 3).join(' · ');
if (parts.length > 3) label += ' +' + (parts.length - 3);
genEditSet(ops, label);
}
function closeModal() { setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); }
function capFull(c) { return (c.caption || '') + '\n\n' + (c.hashtags || []).map(h => '#' + String(h).replace(/^#/, '').replace(/\s+/g, '')).join(' '); }
function loadCaption() {
if (!id) return;
setCap('loading'); setCopied(false);
benchApi('caption', { id }).then(j => setCap(j)).catch(e => { setCap(null); setErr(e.message || 'Could not write a caption.'); });
}
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(); }
}
function genVar() {
if (!id) return;
const n = varN + 1; setVarN(n);
const text = varText.trim();
const key = 'var_' + n;
const label = text ? ('“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”') : ('Variation ' + n);
setMorphs(prev => [{ key, label }].concat(prev));
gen(key, 'variation', { n, instructions: text });
setVarText('');
}
function genForm(form) {
const f = FORMS.find(x => x.k === form), key = 'form_' + form;
if (!morphs.find(m => m.key === key)) setMorphs(prev => [{ key, label: f.label }].concat(prev));
gen(key, 'reform', { form });
}
async function save() {
if (!id || saving || savingRef.current) return;
savingRef.current = true;
setSaving(true); setSavedUrl(''); setSavedN(''); setErr('');
try {
const start = await benchApi('save_start', { id, spec, original: isUrl(src) ? src : '', setid: savedSetRef.current || undefined });
savedSetRef.current = start.setid;
const items = [];
if (isUrl(out.blueprint)) items.push({ key: 'blueprint', label: 'Technical flat', url: out.blueprint });
if (isUrl(out.views)) items.push({ key: 'views', label: 'Front · Back · Detail', url: out.views });
if (isUrl(out.realize)) items.push({ key: 'realize', label: 'Made real', url: out.realize });
if (isUrl(out.rendering)) items.push({ key: 'rendering', label: 'Fashion illustration', url: out.rendering });
shots.forEach(s => { if (isUrl(out[s.key])) items.push({ key: s.key, label: s.placeLabel + ' · ' + s.lookLabel, url: out[s.key] }); });
morphs.forEach(m => { if (isUrl(out[m.key])) items.push({ key: m.key, label: m.label, url: out[m.key] }); });
fixes.forEach(f => { if (isUrl(out[f.key])) items.push({ key: f.key, label: 'Perfected · ' + f.label, url: out[f.key] }); });
let n = 0;
for (const it of items) { await benchApi('save_item', { setid: start.setid, key: it.key, label: it.label, dataUrl: it.url }); n++; setSavedN(n + '/' + items.length); }
try {
const k = 'studio76_apparel_sets', arr = JSON.parse(localStorage.getItem(k) || '[]').filter(s => s.id !== start.setid);
arr.unshift({ id: start.setid, url: start.url, title: (spec && spec.garment_type) || 'Saved look', n: items.length, created: Date.now() });
localStorage.setItem(k, JSON.stringify(arr.slice(0, 60)));
} catch (e) {}
setSavedUrl(start.url);
} catch (e) { setErr(e.message || 'Save failed.'); }
savingRef.current = false;
setSaving(false);
}
function reset() { setStage('idle'); setSrc(null); setId(null); setSpec(null); setOut({}); setShots([]); setMorphs([]); setFixes([]); setFixN(0); setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); aspectsRef.current = {}; setVarN(0); setSaving(false); setSavedN(''); setSavedUrl(''); savedSetRef.current = null; savingRef.current = false; setErr(''); }
const meas = spec && spec.estimated_measurements_cm ? [spec.estimated_measurements_cm.bust && ('bust ' + spec.estimated_measurements_cm.bust), spec.estimated_measurements_cm.waist && ('waist ' + spec.estimated_measurements_cm.waist), spec.estimated_measurements_cm.length && ('length ' + spec.estimated_measurements_cm.length)].filter(Boolean).join(' · ') : '';
const selCount = (sel.length ? 1 : 0) + (sel.neckline ? 1 : 0) + (sel.sleeve ? 1 : 0) + (sel.fabric ? 1 : 0) + (sel.color ? 1 : 0) + (sel.fit ? 1 : 0) + sel.details.length + (sel.note.trim() ? 1 : 0);
const Row = ({ l, v }) => v ?
{l} {v}
: null;
const Tile = ({ keyName, title, sub }) => {
const v = out[keyName];
return (
{v === 'loading'
?
{sub || 'Working…'}
: (typeof v === 'string')
?
{ setModalSrc(v); setModalKey(keyName); setCap(null); setCopied(false); setSel(SEL0); }} />
:
{sub}
}
{title}
{(typeof v === 'string' && v !== 'loading') &&
Download }
);
};
return (
From a photo to a pattern.
Upload any garment — a photo, a screenshot, a runway look you saved. Our atelier reads it, drafts a
technical flat, breaks down the spec, and shows it worn on a model , made to your measurements.
{stage === 'idle' && (
setEntry('piece')}>
A photo of a garment Remake something that already exists
setEntry('sketch')}>
A sketch or idea Bring a drawing to life as a finished piece
)}
{stage === 'idle' && (
fileRef.current && fileRef.current.click()}
onDragOver={e => { e.preventDefault(); setDrag(true); }}
onDragLeave={() => setDrag(false)}
onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0], entry === 'sketch'); }}>
{entry === 'sketch' ? 'Drop your sketch here, or ' : 'Drop a photo here, or '}browse
{entry === 'sketch' ? 'A drawing, croquis or concept — we’ll render it as a finished piece.' : 'JPG / PNG · a clear, well-lit shot works best'}
handleFile(e.target.files[0], entry === 'sketch')} />
)}
{stage === 'idle' && (
setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText, entry === 'sketch'); }} />
handleUrl(urlText, entry === 'sketch')}>Bring it in
)}
{stage === 'idle' && (
✦ Clip a look from Pinterest in one tap ↗
)}
{err && {err}
}
{stage !== 'idle' && (
{saving ? ('Saving… ' + savedN) : (savedUrl ? 'Update this look' : 'Save this look')}
{savedUrl
?
View / share your look ↗
:
Stores the spec, flat & every render at a shareable link. }
gen('blueprint', 'draw')}>Draft tech flat
gen('views', 'views')}>Front · Back · Detail
gen('rendering', 'render_sketch')}>✦ Fashion illustration
✦ See it worn — Instagram-ready
Pick who wears it and a look, then tap a shot. Mix & match to build a campaign.
{MODELS.map(m => (
setModel(m.k)}>{m.label}
))}
{LOOKS.map(l => (
setLook(l.k)}>{l.label}
))}
{WEAR.map(w => {
const k = 'w_' + w.k + '_' + look + '_' + model;
return (
genWear(w.k)}>
{out[k] === 'loading' ? '…' : w.label}
);
})}
{(() => { const k = 'mag_' + look + '_' + model; return (
{out[k] === 'loading' ? '…' : '✦ Magazine cover'}
); })()}
{shots.length > 0 && (
{shots.map(s => )}
)}
✦ Make it a collection — variations & forms
Tell us what to change, or just spin off a fresh take — then reimagine it as a matching dress, top, skirt…
setVarText(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') genVar(); }} />
Create variation
{FORMS.map(f => {
const k = 'form_' + f.k;
return genForm(f.k)}>{out[k] === 'loading' ? '…' : f.label} ;
})}
{morphs.length > 0 && (
{morphs.map(m => )}
)}
{fixes.length > 0 && (
✦ Perfected takes
Refined renders — zoom any image and tell the atelier what to change to make more.
{fixes.map(f => )}
)}
)}
{modalSrc && (
×
e.stopPropagation()}>
{modalKey && isUrl(out[modalKey]) && (
✦ Refine this design
Stack as many changes as you like, then Apply — they’re built in a single pass, the rest of the garment held steady. The new take lands in “Perfected takes”.
Length
setSel(s => ({ ...s, length: s.length === 'longer' ? null : 'longer' }))}>Longer ↓
setSel(s => ({ ...s, length: s.length === 'shorter' ? null : 'shorter' }))}>Shorter ↑
Neckline
{NECKLINES.map(c => { const v = c.toLowerCase(); return setSel(s => ({ ...s, neckline: s.neckline === v ? null : v }))}>{c} ; })}
Sleeves
{SLEEVES.map(st => { const v = st.toLowerCase(); return setSel(s => ({ ...s, sleeve: s.sleeve === v ? null : v }))}>{st} ; })}
Fabric
{FABRICS.map(m => { const v = m.toLowerCase(); return setSel(s => ({ ...s, fabric: s.fabric === v ? null : v }))}>{m} ; })}
Colour
{COLORS.map(c => { const v = c.name.toLowerCase(); return setSel(s => ({ ...s, color: s.color === v ? null : v }))}> {c.name} ; })}
Fit
{FITS.map(m => { const v = m.toLowerCase(); return setSel(s => ({ ...s, fit: s.fit === v ? null : v }))}>{m} ; })}
Detailing
{DETAILS.map(d => = 0 ? ' on' : '')} onClick={() => setSel(s => ({ ...s, details: s.details.indexOf(d.k) >= 0 ? s.details.filter(x => x !== d.k) : s.details.concat(d.k) }))}>{d.label} )}
✦ Apply{selCount ? ' ' + selCount + ' change' + (selCount > 1 ? 's' : '') : ' changes'}
{selCount > 0 && setSel(SEL0)}>clear }
)}
{spec && (
✦ Post to Instagram
{!cap &&
We’ll write the caption & hashtags — download the image and post it in seconds.
}
{!cap &&
✦ Write a caption }
{cap === 'loading' &&
Writing a caption…
}
{cap && cap !== 'loading' && (
)}
)}
)}
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );