← back to CelebritySignatures
public/murals.html
544 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Celebrity Signature Murals — made to order</title>
<style>
:root{
--ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e3dc; --bg:#f7f5f0; --card:#fff;
--accent:#1d7a36; --gold:#9a6b00; --wall:#efe9df; --floor:#d9cfbf;
}
*{box-sizing:border-box}
body{margin:0;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg)}
h1,h2,h3{font-family:Georgia,"Times New Roman",serif;font-weight:600;letter-spacing:.01em}
a{color:var(--accent)}
header{padding:24px 30px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:30;display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}
header h1{margin:0;font-size:24px}
header .sub{color:var(--muted);font-size:13px}
header nav{margin-left:auto;display:flex;gap:18px;font-size:14px}
header nav a{color:var(--muted);text-decoration:none}
header nav a:hover{color:var(--ink)}
main{max-width:1180px;margin:0 auto;padding:30px 30px 80px}
.intro{max-width:760px;margin-bottom:30px}
.intro h2{font-size:30px;margin:0 0 10px}
.intro p{color:#3c3c3c;margin:0 0 8px}
.spec{display:flex;gap:26px;flex-wrap:wrap;margin-top:16px;padding:14px 18px;background:#fff;border:1px solid var(--line);border-radius:12px}
.spec div{font-size:13px}
.spec b{display:block;font-size:18px;font-family:Georgia,serif}
/* gallery */
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:22px;margin-top:10px}
.m-card{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden;display:flex;flex-direction:column}
.m-card .pic{background:#fff;border-bottom:1px solid var(--line);padding:14px;display:flex;align-items:center;justify-content:center;aspect-ratio:2/1;overflow:hidden}
.m-card .pic img{max-width:100%;max-height:100%;object-fit:contain}
.m-card .body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:8px;flex:1}
.m-card h3{margin:0;font-size:19px}
.m-card .blurb{color:var(--muted);font-size:13.5px;flex:1}
.m-card .facts{display:flex;gap:8px;flex-wrap:wrap;font-size:11.5px;color:#555}
.pill{padding:3px 10px;border-radius:999px;background:#f0ece3;border:1px solid var(--line)}
.m-card .foot{display:flex;align-items:center;gap:10px;margin-top:4px}
.price{font-family:Georgia,serif;font-size:22px}
.btn{appearance:none;border:1px solid var(--ink);background:var(--ink);color:#fff;font:inherit;font-size:14px;padding:9px 16px;border-radius:9px;cursor:pointer;text-decoration:none;display:inline-block}
.btn.ghost{background:#fff;color:var(--ink)}
.btn:hover{opacity:.9}
.btn.full{width:100%;text-align:center}
/* studio */
.studio{margin-top:46px;border-top:1px solid var(--line);padding-top:34px;scroll-margin-top:90px}
.studio h2{font-size:26px;margin:0 0 4px}
.studio .lead{color:var(--muted);margin:0 0 18px;max-width:680px}
.stage-wrap{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px}
.ctrls{display:flex;gap:22px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
.ctrls .f{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted)}
.ctrls select,.ctrls input[type=range]{font:inherit}
.ctrls select{padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font-size:14px}
.ctrls output{font-family:Georgia,serif;color:var(--ink);font-size:15px}
.scene{position:relative;width:100%;background:linear-gradient(#f4eee4,#efe9df);border:1px solid var(--line);border-radius:10px;overflow:hidden;user-select:none;touch-action:none}
.floor{position:absolute;left:0;right:0;bottom:0;background:var(--floor);border-top:2px solid #c9bda9}
.baseboard{position:absolute;left:0;right:0;background:#fff;border-top:1px solid #cfc4b2;border-bottom:1px solid #cfc4b2}
.human{position:absolute;bottom:0;width:14px;background:#cdbfa9;border-radius:7px 7px 0 0;opacity:.85}
.human::after{content:"6 ft";position:absolute;top:-18px;left:50%;transform:translateX(-50%);font-size:10px;color:#8a7c63;white-space:nowrap}
.mural{position:absolute;cursor:grab;border:1px solid rgba(0,0,0,.18);box-shadow:0 6px 22px rgba(60,50,30,.22);background-size:cover;background-position:center}
.mural.drag{cursor:grabbing;box-shadow:0 10px 30px rgba(60,50,30,.32)}
.mural .grip{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;letter-spacing:.08em;text-shadow:0 1px 3px rgba(0,0,0,.5);opacity:0;transition:.15s}
.mural:hover .grip{opacity:.9}
.dims{position:absolute;font-size:11px;color:#8a7c63;pointer-events:none}
.readout{display:flex;gap:24px;flex-wrap:wrap;margin-top:14px;font-size:13px}
.readout b{font-family:Georgia,serif;font-weight:600;font-size:15px}
.hint{font-size:12px;color:var(--muted);margin-top:8px}
/* order */
.order{margin-top:40px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:22px 24px;max-width:680px}
.order h2{font-size:22px;margin:0 0 6px}
.order p{color:var(--muted);margin:0 0 16px;font-size:14px}
.order form{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.order label{display:flex;flex-direction:column;gap:5px;font-size:12px;color:var(--muted)}
.order label.wide{grid-column:1/3}
.order input,.order textarea{font:inherit;padding:9px 11px;border:1px solid var(--line);border-radius:8px;background:#fff}
.order textarea{min-height:64px;resize:vertical}
.order .summary{grid-column:1/3;background:var(--bg);border:1px dashed var(--line);border-radius:10px;padding:12px 14px;font-size:13px}
.order .summary b{font-family:Georgia,serif}
.msg{grid-column:1/3;font-size:14px}
.msg.ok{color:var(--accent)} .msg.err{color:#b3261e}
footer{max-width:1180px;margin:0 auto;padding:0 30px 50px;color:var(--muted);font-size:12.5px;line-height:1.7}
@media(max-width:620px){.order form{grid-template-columns:1fr}.order label.wide,.order .summary,.msg{grid-column:1}}
/* — name finder (consensus component) — */
.finder{margin-top:18px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:15px 17px}
.finder .lab{font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px}
.finder .row{display:flex;gap:10px;flex-wrap:wrap}
.finder input{flex:1;min-width:230px;font:inherit;padding:10px 12px;border:1px solid var(--line);border-radius:8px}
.finder .res{margin-top:11px;display:flex;flex-direction:column;gap:7px;font-size:14px}
.finder .hit{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.finder .hit img{height:26px;max-width:96px;object-fit:contain}
.finder .hit b{font-family:Georgia,serif;font-weight:600}
.finder .miss{color:var(--muted)}
/* — named-toggle pill on card image — */
.name-toggle{position:absolute;top:10px;right:10px;font-size:11.5px;padding:4px 11px;border-radius:999px;background:rgba(255,255,255,.93);border:1px solid var(--line);cursor:pointer;color:var(--ink);z-index:2}
.name-toggle:hover{background:#fff}
/* — roster drawer — */
.roster-trig{background:none;border:none;color:var(--accent);font:inherit;font-size:12.5px;cursor:pointer;padding:0;text-decoration:underline;align-self:flex-start}
.roster{margin-top:6px;border-top:1px dashed var(--line);padding-top:10px;display:none}
.roster.open{display:block}
.roster .r-search{width:100%;font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--line);border-radius:7px;margin-bottom:8px}
.roster .r-list{display:grid;grid-template-columns:1fr 1fr;gap:5px;max-height:260px;overflow:auto}
.roster .r-item{display:flex;align-items:center;gap:8px;font-size:12.5px;padding:4px 6px;border-radius:6px;cursor:pointer;text-align:left;background:none;border:1px solid transparent;width:100%;font-family:inherit;color:var(--ink)}
.roster .r-item:hover{background:#f4f0e8;border-color:var(--line)}
.roster .r-item img{height:20px;width:50px;object-fit:contain;flex:none}
.roster .r-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* — provenance modal — */
.modal{position:fixed;inset:0;background:rgba(20,18,14,.55);display:none;align-items:center;justify-content:center;z-index:60;padding:20px}
.modal.open{display:flex}
.modal .box{background:#fff;border-radius:14px;max-width:440px;width:100%;padding:22px 24px;position:relative}
.modal .x{position:absolute;top:10px;right:14px;border:none;background:none;font-size:22px;cursor:pointer;color:var(--muted);line-height:1}
.modal .sig{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px;display:flex;align-items:center;justify-content:center;min-height:90px;margin-bottom:14px}
.modal .sig img{max-width:100%;max-height:120px;object-fit:contain}
.modal h3{margin:0 0 4px;font-size:21px}
.modal .why{color:var(--muted);font-size:13px;margin:0 0 14px}
.modal .prov{display:grid;grid-template-columns:auto 1fr;gap:7px 14px;font-size:13px;margin:0}
.modal .prov dt{color:var(--muted)}
.modal .prov dd{margin:0}
.modal .badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:#eaf5ec;color:#1d7a36;border:1px solid #cfe8d5}
.modal .badge.medium{background:#fdf4e3;color:#9a6b00;border-color:#f0e2c0}
.modal .badge.high{background:#fdecea;color:#b3261e;border-color:#f3cdc8}
/* — scene presets in studio — */
.scene-presets{display:flex;gap:6px;flex-wrap:wrap}
.scene-presets button{font:inherit;font-size:12.5px;padding:6px 11px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;color:var(--muted)}
.scene-presets button.on{border-color:var(--ink);color:var(--ink);background:#f4f0e8}
.scene .swindow{position:absolute;top:8%;background:linear-gradient(160deg,#dCE7f0,#eef3f7);border:3px solid #fff;box-shadow:0 0 0 1px #c9bda9, 0 8px 20px rgba(0,0,0,.12)}
.scene .sart{position:absolute;top:14%;background:#fff;border:4px solid #fff;box-shadow:0 0 0 1px #c9bda9,0 6px 16px rgba(0,0,0,.14)}
</style>
</head>
<body>
<header>
<h1>Celebrity Signature Murals</h1>
<span class="sub">made to order · Designer Wallcoverings</span>
<nav>
<a href="/">Signature archive</a>
<a href="#studio">Wall studio</a>
<a href="#order">Order</a>
</nav>
</header>
<main>
<section class="intro">
<h2>A wall of history, in their own hand.</h2>
<p>Each mural is a curated wall of authentic signatures — every name traced to a
public-domain or openly licensed source, no signature invented. Printed to order
as a single seamless piece for a full feature wall.</p>
<div class="spec" id="spec"><!-- filled by JS --></div>
<div class="finder">
<p class="lab">Is your icon on a wall?</p>
<div class="row">
<input id="finderInput" type="search" placeholder="Search any name — e.g. Lincoln, Chaplin, Babe Ruth…" autocomplete="off">
</div>
<div class="res" id="finderRes"></div>
</div>
</section>
<div class="gallery" id="gallery"></div>
<!-- WALL PLACEMENT STUDIO -->
<section class="studio" id="studio">
<h2>Place it on your wall</h2>
<p class="lead">Pick a mural, set your wall width, then <b>drag the mural</b> to exactly
where you want it. The 6 ft figure is there for scale.</p>
<div class="stage-wrap">
<div class="ctrls">
<div class="f">Mural
<select id="selMural"></select>
</div>
<div class="f">Wall width — <output id="wwOut"></output>
<input id="wallW" type="range" min="24" max="60" step="1" value="32">
</div>
<div class="f">Wall height — <output id="whOut"></output>
<input id="wallH" type="range" min="12" max="20" step="1" value="14">
</div>
<div class="f">Room scene
<div class="scene-presets" id="scenePresets"></div>
</div>
<div class="f">
<button class="btn ghost" id="btnCenter" type="button">Center on wall</button>
</div>
</div>
<div class="scene" id="scene"></div>
<div class="readout" id="readout"></div>
<div class="hint">Mural is fixed at 24 ft wide × 12 ft tall (150 DPI print master). Drag to set horizontal & vertical placement; it can't leave the wall.</div>
</div>
</section>
<!-- ORDER -->
<section class="order" id="order">
<h2>Order this mural</h2>
<p>Tell us your wall and we'll prepare your 150 DPI print master and a quote.</p>
<form id="orderForm">
<label>Your name<input name="name" required></label>
<label>Email<input name="email" type="email" required></label>
<label>Wall width (ft)<input name="wall_w" type="number" min="1" step="0.5" required></label>
<label>Wall height (ft)<input name="wall_h" type="number" min="1" step="0.5" required></label>
<label class="wide">Notes (room, install date, anything)<textarea name="notes"></textarea></label>
<div class="summary" id="orderSummary"></div>
<div style="grid-column:1/3"><button class="btn" type="submit">Request this mural</button></div>
<div class="msg" id="orderMsg"></div>
</form>
</section>
</main>
<!-- provenance modal (consensus component) -->
<div class="modal" id="provModal">
<div class="box">
<button class="x" id="provX" type="button" aria-label="Close">×</button>
<div class="sig" id="provSig"></div>
<h3 id="provName"></h3>
<p class="why" id="provWhy"></p>
<dl class="prov" id="provFields"></dl>
</div>
</div>
<footer>
Every signature on these murals is sourced from Wikimedia Commons under a public-domain or
open license and is rendered at uniform size — no signature is emphasized over another.
Living-celebrity signatures are excluded (right of publicity). Print masters are produced to
order at 150 DPI; final clarity scales with viewing distance for large-format installs.
</footer>
<script>
const FT = " ft";
let CAT = null; // catalog
let cur = null; // current mural in studio
let pos = {x:0, y:0}; // mural top-left in FEET on the wall
// signature archive (powers the consensus components: finder, roster, provenance)
const CATEGORY_OF = {
'oldest-signatures':'Oldest Signatures',
'declaration-of-independence':'Declaration of Independence', 'politics':'Politics',
'sports':'Sports', 'hollywood':'Hollywood', 'movies-classic':'Movies (Classic)', 'tv':'TV'
};
let SIGS = []; // usable=yes rows only (what's actually on the murals)
let byMural = {}; // mural code -> [rows] (rank-sorted)
let sigByKey = {}; // "category|rank" -> row
// room-scene presets for the wall studio (CSS-only, $0)
const SCENES = {
plain: {label:'Plain', wall:'linear-gradient(#f4eee4,#efe9df)', floor:'#d9cfbf', extra:''},
study: {label:'Study', wall:'linear-gradient(#e9ddc9,#ddcdb2)', floor:'#7c5a3a', extra:'art'},
lounge: {label:'Lounge', wall:'linear-gradient(#e7e8e3,#d8dad2)', floor:'#b29a7c', extra:'window'},
gallery: {label:'Gallery', wall:'linear-gradient(#fafafa,#eeeeec)', floor:'#cfcabf', extra:''},
};
let curScene = 'plain';
const money = n => "$" + Math.round(n).toLocaleString();
const $ = s => document.querySelector(s);
async function boot(){
CAT = await (await fetch('/api/murals-catalog')).json();
const {widthFt,heightFt,dpi,pxW,pxH,sqft} = CAT.size;
const price = CAT.pricePerSqFt * sqft;
$('#spec').innerHTML = `
<div><b>${heightFt} × ${widthFt} ft</b>height × width</div>
<div><b>${dpi} DPI</b>${pxH.toLocaleString()} × ${pxW.toLocaleString()} px master</div>
<div><b>${sqft} sq ft</b>seamless feature wall</div>
<div><b>${money(price)}</b>from, ${CAT.material.toLowerCase()}</div>`;
// gallery
$('#gallery').innerHTML = CAT.murals.map(m => `
<div class="m-card" data-code="${m.code}">
<div class="pic">
<button class="name-toggle" data-toggle="${m.code}" type="button">Show names</button>
<img loading="lazy" data-base="/output/collage-${m.slug}.png" data-named="/output/collage-${m.slug}-named.png"
src="/output/collage-${m.slug}.png" alt="${m.title} signature mural">
</div>
<div class="body">
<h3>${m.title}</h3>
<div class="blurb">${m.blurb}</div>
<div class="facts">
<span class="pill">${m.sigs} signatures</span>
<span class="pill">${heightFt}×${widthFt} ft</span>
<span class="pill">${dpi} DPI TIF</span>
</div>
<button class="roster-trig" data-roster="${m.code}" type="button">See who's on this wall ▾</button>
<div class="roster" data-rosterbody="${m.code}"></div>
<div class="foot">
<span class="price">${money(price)}</span>
<button class="btn ghost" data-place="${m.code}" type="button">Place on wall →</button>
</div>
</div>
</div>`).join('');
// mural selector
$('#selMural').innerHTML = CAT.murals.map(m => `<option value="${m.code}">${m.title}</option>`).join('');
// load the signature archive (usable rows only) → finder, roster, provenance
try{
const all = await (await fetch('/api/signatures')).json();
SIGS = all.filter(r => r.usable_in_commercial_collage === 'yes');
for(const r of SIGS) sigByKey[r.category + '|' + r.rank] = r;
for(const m of CAT.murals){
if(CATEGORY_OF[m.slug]){
byMural[m.code] = SIGS.filter(r => r.category === CATEGORY_OF[m.slug]).sort((a,b)=>a.rank-b.rank);
} else if(Array.isArray(m.roster)){
// themed murals carry their resolved roster in the catalog entry
byMural[m.code] = m.roster.map((nm,i)=>({full_name:nm, category:m.slug, rank:i+1, signature_image_url:''}));
} else {
byMural[m.code] = [];
}
}
}catch(e){ SIGS = []; }
// gallery actions: place-on-wall · named toggle · roster drawer · roster item → provenance
$('#gallery').addEventListener('click', e => {
const place = e.target.closest?.('[data-place]');
if(place){ const code=place.getAttribute('data-place'); $('#selMural').value=code; selectMural(code); $('#studio').scrollIntoView({behavior:'smooth'}); return; }
const tog = e.target.closest?.('[data-toggle]');
if(tog){ const img=tog.parentElement.querySelector('img'); const showing=tog.textContent.startsWith('Show');
img.src = showing ? img.dataset.named : img.dataset.base; tog.textContent = showing ? 'Hide names' : 'Show names'; return; }
const rt = e.target.closest?.('[data-roster]');
if(rt){ toggleRoster(rt.getAttribute('data-roster'), rt); return; }
const item = e.target.closest?.('[data-sig]');
if(item){ openProv(sigByKey[item.getAttribute('data-sig')]); return; }
});
$('#gallery').addEventListener('input', e => {
const rs = e.target.closest?.('.r-search'); if(!rs) return;
const q = rs.value.trim().toLowerCase();
rs.closest('.roster').querySelectorAll('.r-item').forEach(it =>
it.style.display = it.textContent.toLowerCase().includes(q) ? '' : 'none');
});
buildScenePresets();
wireFinder();
$('#provX').addEventListener('click', ()=>$('#provModal').classList.remove('open'));
$('#provModal').addEventListener('click', e => { if(e.target.id==='provModal') $('#provModal').classList.remove('open'); });
document.addEventListener('keydown', e => { if(e.key==='Escape') $('#provModal').classList.remove('open'); });
$('#selMural').addEventListener('change', e => selectMural(e.target.value));
$('#wallW').addEventListener('input', drawScene);
$('#wallH').addEventListener('input', drawScene);
$('#btnCenter').addEventListener('click', centerMural);
window.addEventListener('resize', drawScene);
selectMural(CAT.murals[0].code);
wireOrder();
}
// — roster drawer (consensus #1) —
function toggleRoster(code, trig){
const body = document.querySelector(`[data-rosterbody="${code}"]`);
const open = body.classList.toggle('open');
trig.textContent = (open ? "Hide who's on this wall ▴" : "See who's on this wall ▾");
if(open && !body.dataset.filled){
const rows = byMural[code] || [];
body.innerHTML =
`<input class="r-search" type="search" placeholder="Filter ${rows.length} names…" autocomplete="off">
<div class="r-list">` +
rows.map(r => `<button class="r-item" data-sig="${r.category}|${r.rank}" type="button">
<img loading="lazy" src="${r.signature_image_url}" alt=""><span>${r.full_name}</span></button>`).join('') +
`</div>`;
body.dataset.filled = '1';
}
}
// — provenance modal (consensus #2) —
function openProv(row){
if(!row) return;
$('#provSig').innerHTML = `<img src="${row.signature_image_url}" alt="${row.full_name} signature">`;
$('#provName').textContent = row.full_name;
$('#provWhy').textContent = row.reason_for_ranking || '';
const deceased = row.deceased ? ('Deceased' + (row.death_date ? ' ' + String(row.death_date).slice(0,4) : '')) : 'Living';
const risk = (row.risk_level || 'low').toLowerCase();
const link = row.wikidata || (row.ranking_source_urls||'').split('|')[0].trim() || row.signature_image_url;
$('#provFields').innerHTML = `
<dt>License</dt><dd>${row.image_license || '—'}</dd>
<dt>Source</dt><dd>${row.signature_source_type || 'Wikimedia Commons'}</dd>
<dt>Status</dt><dd>${deceased}</dd>
<dt>Use</dt><dd><span class="badge ${risk}">${risk} risk · collage-cleared</span></dd>
<dt>Provenance</dt><dd><a href="${link}" target="_blank" rel="noopener noreferrer">Wikidata / source ↗</a></dd>`;
$('#provModal').classList.add('open');
}
// — name finder (consensus #3) —
function wireFinder(){
const inp = $('#finderInput'), res = $('#finderRes');
const codeOfCat = {}; CAT.murals.forEach(m => codeOfCat[CATEGORY_OF[m.slug]] = m);
inp.addEventListener('input', () => {
const q = inp.value.trim().toLowerCase();
if(q.length < 2){ res.innerHTML = ''; return; }
const hits = SIGS.filter(r => r.full_name.toLowerCase().includes(q)).slice(0, 8);
if(!hits.length){ res.innerHTML = `<div class="miss">Not in the openly-licensed archive yet — <a href="#order">request a custom mural</a> and we'll source them.</div>`; return; }
res.innerHTML = hits.map(r => { const m = codeOfCat[r.category];
return `<div class="hit"><img src="${r.signature_image_url}" alt=""> <b>${r.full_name}</b> — on ${m ? `<a href="#" data-go="${m.code}">${m.title}</a>` : r.category}</div>`;
}).join('');
});
res.addEventListener('click', e => {
const go = e.target.closest?.('[data-go]'); if(!go) return;
e.preventDefault(); const code = go.getAttribute('data-go');
$('#selMural').value = code; selectMural(code); $('#studio').scrollIntoView({behavior:'smooth'});
});
}
// — room-scene presets (consensus #4) —
function buildScenePresets(){
$('#scenePresets').innerHTML = Object.entries(SCENES).map(([k,v]) =>
`<button type="button" data-scene="${k}" class="${k===curScene?'on':''}">${v.label}</button>`).join('');
$('#scenePresets').addEventListener('click', e => {
const b = e.target.closest('[data-scene]'); if(!b) return;
curScene = b.getAttribute('data-scene');
$('#scenePresets').querySelectorAll('button').forEach(x => x.classList.toggle('on', x===b));
drawScene();
});
}
function muralByCode(c){ return CAT.murals.find(m => m.code === c); }
function selectMural(code){
cur = muralByCode(code);
centerMural(); // also draws
}
function wallDims(){ return { W:+$('#wallW').value, H:+$('#wallH').value }; }
function centerMural(){
const {W,H} = wallDims();
const mw = CAT.size.widthFt, mh = CAT.size.heightFt;
pos.x = Math.max(0,(W-mw)/2);
pos.y = Math.max(0,(H-mh)/2);
drawScene();
}
function drawScene(){
if(!cur) return;
const scene = $('#scene');
const {W,H} = wallDims();
const mw = CAT.size.widthFt, mh = CAT.size.heightFt;
// scale: fit wall width to scene width; cap scene height sensibly
const sw = scene.clientWidth || 900;
let pxPerFt = sw / W;
const sceneH = H * pxPerFt;
scene.style.height = sceneH + "px";
// clamp position to wall
pos.x = Math.min(Math.max(0,pos.x), Math.max(0,W-mw));
pos.y = Math.min(Math.max(0,pos.y), Math.max(0,H-mh));
$('#wwOut').textContent = W + FT;
$('#whOut').textContent = H + FT;
const floorH = Math.min(sceneH*0.10, pxPerFt*1.0);
const SC = SCENES[curScene] || SCENES.plain;
scene.style.background = SC.wall;
let extra = '';
if(SC.extra === 'window') extra = `<div class="swindow" style="right:7%;width:${pxPerFt*3.5}px;height:${pxPerFt*4.5}px"></div>`;
else if(SC.extra === 'art') extra = `<div class="sart" style="left:6%;width:${pxPerFt*2}px;height:${pxPerFt*2.6}px"></div>`;
scene.innerHTML = `
<div class="floor" style="height:${floorH}px;background:${SC.floor}"></div>
${extra}
<div class="baseboard" style="bottom:${floorH}px;height:${Math.max(6,pxPerFt*0.4)}px"></div>
<div class="human" style="left:${pxPerFt*1.2}px;height:${6*pxPerFt - floorH}px;bottom:${floorH}px"></div>
<div class="mural" id="muralEl"
style="width:${mw*pxPerFt}px;height:${mh*pxPerFt}px;
left:${pos.x*pxPerFt}px;top:${(H-mh-pos.y)*pxPerFt}px;
background-image:url('/output/collage-${cur.slug}.png')">
<div class="grip">⟵ drag ⟶</div>
</div>`;
enableDrag(pxPerFt);
const floorGap = pos.y; // ft from mural bottom to floor
const leftGap = pos.x; // ft from left wall to mural
const rightGap = Math.max(0, W - mw - pos.x);
$('#readout').innerHTML = `
<div>From left wall: <b>${leftGap.toFixed(1)}${FT}</b></div>
<div>From right wall: <b>${rightGap.toFixed(1)}${FT}</b></div>
<div>Bottom off floor: <b>${floorGap.toFixed(1)}${FT}</b></div>
<div>Mural: <b>${mh}×${mw}${FT}</b></div>`;
syncOrderSummary();
}
function enableDrag(pxPerFt){
const el = $('#muralEl');
let start=null;
el.addEventListener('pointerdown', e => {
start = {mx:e.clientX, my:e.clientY, px:pos.x, py:pos.y};
el.setPointerCapture(e.pointerId); el.classList.add('drag');
});
el.addEventListener('pointermove', e => {
if(!start) return;
const dxFt = (e.clientX-start.mx)/pxPerFt;
const dyFt = (e.clientY-start.my)/pxPerFt;
pos.x = start.px + dxFt;
pos.y = start.py - dyFt; // up = increase floor gap
const {W,H}=wallDims(), mw=CAT.size.widthFt, mh=CAT.size.heightFt;
pos.x = Math.min(Math.max(0,pos.x), Math.max(0,W-mw));
pos.y = Math.min(Math.max(0,pos.y), Math.max(0,H-mh));
el.style.left = pos.x*pxPerFt + "px";
el.style.top = (H-mh-pos.y)*pxPerFt + "px";
liveReadout();
});
const end = e => { if(start){ start=null; el.classList.remove('drag'); syncOrderSummary(); } };
el.addEventListener('pointerup', end);
el.addEventListener('pointercancel', end);
}
function liveReadout(){
const {W}=wallDims(), mw=CAT.size.widthFt;
$('#readout').children[0].querySelector('b').textContent = pos.x.toFixed(1)+FT;
$('#readout').children[1].querySelector('b').textContent = Math.max(0,W-mw-pos.x).toFixed(1)+FT;
$('#readout').children[2].querySelector('b').textContent = pos.y.toFixed(1)+FT;
}
function syncOrderSummary(){
if(!cur) return;
const {W,H}=wallDims();
const price = CAT.pricePerSqFt * CAT.size.sqft;
$('#orderSummary').innerHTML =
`<b>${cur.title}</b> · ${CAT.size.heightFt}×${CAT.size.widthFt}${FT} · ${money(price)}<br>
Placement on a ${W}×${H}${FT} wall: ${pos.x.toFixed(1)}${FT} from left, ${pos.y.toFixed(1)}${FT} off floor.`;
// mirror into order form if blank
const f = $('#orderForm');
if(!f.wall_w.value) f.wall_w.value = W;
if(!f.wall_h.value) f.wall_h.value = H;
}
function wireOrder(){
$('#orderForm').addEventListener('submit', async e => {
e.preventDefault();
const msg = $('#orderMsg'); msg.className='msg'; msg.textContent='Sending…';
const f = e.target;
const payload = {
mural: cur?.slug, mural_title: cur?.title,
name: f.name.value, email: f.email.value,
wall_w: +f.wall_w.value, wall_h: +f.wall_h.value,
placement: { from_left_ft:+pos.x.toFixed(2), off_floor_ft:+pos.y.toFixed(2) },
size: CAT.size, price: CAT.pricePerSqFt*CAT.size.sqft,
notes: f.notes.value
};
try{
const r = await fetch('/api/mural-order',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const j = await r.json();
if(j.ok){ msg.className='msg ok'; msg.textContent='Thank you — request #'+j.id+' received. We’ll email you a proof and quote.'; f.reset(); syncOrderSummary(); }
else { msg.className='msg err'; msg.textContent = j.error || 'Something went wrong.'; }
}catch(err){ msg.className='msg err'; msg.textContent='Network error — please email steve@designerwallcoverings.com.'; }
});
}
boot();
</script>
</body>
</html>