← back to Hollywood Wallcoverings
customcreator/configure.html
490 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Mural Configurator — Hollywood Wallcoverings</title>
<style>
:root{
--ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e2db; --bg:#fbfaf7; --paper:#fff;
--accent:#c9a865; --accent-ink:#a98b50; --radius:4px;
--serif:'Cormorant Garamond',Georgia,'Times New Roman',serif;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.55;-webkit-font-smoothing:antialiased}
.wrap{max-width:1320px;margin:0 auto;padding:30px 28px 90px}
.crumb{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:22px}
.crumb a{color:var(--muted);text-decoration:none}
.pdp{display:grid;grid-template-columns:1.18fr .82fr;gap:46px;align-items:start}
@media(max-width:920px){.pdp{grid-template-columns:1fr;gap:30px}}
.stage{position:sticky;top:18px}
.tabs{display:inline-flex;border:1px solid var(--line);border-radius:22px;overflow:hidden;margin-bottom:12px}
.tabs button{border:0;background:var(--paper);padding:7px 16px;font-size:12px;letter-spacing:.05em;cursor:pointer;color:var(--muted)}
.tabs button.on{background:var(--ink);color:#fff}
.photos .hero-img{width:100%;aspect-ratio:4/5;object-fit:cover;background:#eee;border-radius:var(--radius);display:block}
.thumbs{display:flex;gap:9px;margin-top:11px;flex-wrap:wrap}
.thumbs img{width:64px;height:80px;object-fit:cover;border-radius:3px;cursor:pointer;opacity:.5;border:1px solid var(--line);transition:opacity .15s}
.thumbs img.active,.thumbs img:hover{opacity:1}
.hide{display:none!important}
/* technical wall preview */
.preview-frame{background:#f3f1ec;border:1px solid var(--line);border-radius:var(--radius);padding:22px;display:flex;align-items:center;justify-content:center;min-height:360px}
.preview{position:relative;max-width:100%;max-height:60vh;box-shadow:0 6px 26px rgba(0,0,0,.16);background-size:cover;background-position:center;outline:1px solid rgba(0,0,0,.12)}
.preview .seam{position:absolute;top:0;bottom:0;width:0;border-left:1px dashed rgba(255,255,255,.85);box-shadow:0 0 0 .5px rgba(0,0,0,.25)}
.preview .dropnum{position:absolute;top:8px;transform:translateX(-50%);font-size:11px;font-weight:600;color:#fff;background:rgba(0,0,0,.45);padding:1px 6px;border-radius:10px}
.preview .trim{position:absolute;border:1px dashed rgba(255,80,80,.95);pointer-events:none}
.preview .trim-lbl{position:absolute;font-size:10px;color:#c0392b;background:rgba(255,255,255,.85);padding:0 4px;border-radius:3px}
.ov{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.ov button{border:1px solid var(--line);background:#fff;border-radius:20px;padding:5px 13px;font-size:11.5px;cursor:pointer;color:var(--muted)}
.ov button.on{border-color:var(--ink);background:var(--ink);color:#fff}
/* PHOTOREAL room visualizer */
.room-frame{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--bg)}
.room{position:relative;width:100%;aspect-ratio:3/2;overflow:hidden;background-size:cover;background-position:center;background-color:#e9e2d4}
.room-wall{position:absolute;overflow:hidden}
.room-mural{position:absolute;bottom:0;left:50%;transform:translateX(-50%);background-size:cover;background-position:center bottom;transition:width .1s,height .1s}
.room-fg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none}
.room-cap{position:absolute;left:12px;bottom:10px;font-size:11px;letter-spacing:.04em;color:#fff;background:rgba(0,0,0,.42);padding:3px 9px;border-radius:12px}
.room-sel{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.room-sel button{border:1px solid var(--line);background:#fff;border-radius:20px;padding:6px 14px;font-size:12px;cursor:pointer;color:var(--muted)}
.room-sel button.on{border-color:var(--ink);background:var(--ink);color:#fff}
.rz{position:absolute;right:-10px;bottom:-10px;width:20px;height:20px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.35);cursor:nwse-resize;display:none;touch-action:none;z-index:3}
.room-mural.custom{cursor:grab;bottom:auto;background-position:center;box-shadow:0 0 0 1px rgba(255,255,255,.75),0 8px 24px rgba(0,0,0,.32);touch-action:none}
.room-mural.custom:active{cursor:grabbing}
.room-mural.custom .rz{display:block}
.cust-ctrl{display:flex;align-items:center;gap:16px;margin-top:11px;font-size:12px;color:var(--muted);flex-wrap:wrap}
.cust-ctrl label{display:flex;align-items:center;gap:7px}
.cust-ctrl input[type=range]{width:110px;-webkit-appearance:none;appearance:none;height:4px;border-radius:3px;background:var(--line)}
.cust-ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;border-radius:50%;background:var(--accent);border:2px solid #fff;cursor:pointer}
.cust-ctrl button{border:1px solid var(--line);background:#fff;border-radius:3px;padding:5px 11px;font-size:12px;cursor:pointer;color:var(--ink)}
.room-sel button.upload{border-style:dashed;color:var(--accent-ink)}
.room-pick{display:flex;gap:8px;margin-top:10px;overflow-x:auto;padding-bottom:4px}
.room-pick button{flex:0 0 auto;width:52px;height:52px;border-radius:5px;border:2px solid transparent;background-size:cover;background-position:center;cursor:pointer;opacity:.8}
.room-pick button.on{border-color:var(--ink);opacity:1}
.panel{max-width:470px}
.vendor{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-ink);font-weight:600}
h1{font-family:var(--serif);font-weight:500;font-size:38px;line-height:1.05;margin:8px 0 6px}
.colors{display:flex;align-items:center;gap:10px;margin:12px 0 16px;flex-wrap:wrap}
.chip{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
.chip i{width:15px;height:15px;border-radius:50%;border:1px solid var(--line);display:inline-block}
.desc{font-size:14.5px;color:#3a3a3a;line-height:1.6;margin:0 0 22px}
.calc{background:var(--paper);border:1px solid var(--line);border-radius:var(--radius);padding:22px}
.calc h2{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600;margin-bottom:14px}
.unit-tog{display:inline-flex;border:1px solid var(--line);border-radius:20px;overflow:hidden;margin-bottom:16px}
.unit-tog button{border:0;background:var(--paper);padding:5px 15px;font-size:12px;cursor:pointer;color:var(--muted)}
.unit-tog button.on{background:var(--ink);color:#fff}
.dims{display:grid;grid-template-columns:1fr auto 1fr;align-items:end;gap:10px}
.field label{display:block;font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:5px}
.field input{width:100%;border:1px solid var(--line);border-radius:3px;padding:11px 12px;font-size:16px;background:#fff;font-family:var(--sans)}
.field input:focus{outline:none;border-color:var(--accent)}
.times{padding-bottom:12px;color:var(--muted)}
.sliders{display:flex;flex-direction:column;gap:18px}
.srow .shead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px}
.srow label{font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}
.srow .sval{font-family:var(--serif);font-size:21px;color:var(--ink)}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:3px;background:var(--line);outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:21px;height:21px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.28)}
input[type=range]::-moz-range-thumb{width:21px;height:21px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.28)}
input[type=range]:focus{outline:none}
.presets{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 4px}
.presets button{border:1px solid var(--line);background:#fff;border-radius:3px;padding:6px 11px;font-size:12px;cursor:pointer;color:var(--ink)}
.presets button:hover{border-color:var(--ink)}
.hint{font-size:11.5px;color:var(--muted);margin:8px 2px 0;line-height:1.5}
.readout{margin:16px 0 4px;border-top:1px solid var(--line)}
.readout div{display:flex;justify-content:space-between;padding:8px 2px;font-size:13px;border-bottom:1px solid var(--line)}
.readout dt{color:var(--muted)} .readout dd{font-weight:600}
.price-block{display:flex;align-items:baseline;justify-content:space-between;margin:16px 0 2px}
.price-block .lbl{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.price{font-family:var(--serif);font-size:40px;font-weight:500;line-height:1}
.price small{font-size:15px;color:var(--muted)}
.perm2{font-size:12px;color:var(--muted);text-align:right;margin-bottom:16px}
.cta{display:block;width:100%;text-align:center;border:0;border-radius:3px;padding:15px;font-size:14px;letter-spacing:.05em;cursor:pointer;font-family:var(--sans)}
.cta.primary{background:var(--accent);color:#fff;font-weight:600;margin-bottom:10px}
.cta.primary:hover{background:var(--accent-ink)} .cta.primary:disabled{opacity:.6;cursor:wait}
.cta.ghost{background:#fff;color:var(--ink);border:1px solid var(--line)} .cta.ghost:hover{border-color:var(--ink)}
.note{font-size:12px;color:var(--muted);margin-top:13px;line-height:1.5}
.sec-title{font-family:var(--serif);font-size:22px;margin:32px 0 4px}
.specs{border-top:1px solid var(--line)}
.specs div{display:flex;justify-content:space-between;padding:12px 2px;border-bottom:1px solid var(--line);font-size:13.5px}
.specs dt{color:var(--muted)} .specs dd{text-align:right;max-width:62%}
.loading{opacity:.4}
/* ===== Above-the-fold fit — whole configurator on one screen, no page scroll ===== */
html,body{height:100%}
body{overflow:hidden}
/* render the entire UI permanently at 80% (reduced 20% from original, like a browser zoom-out) */
:root{--page-zoom:.8}
html{zoom:var(--page-zoom)}
/* divide the fold height by the zoom so the 80%-scaled page still fills the screen (no bottom gap) */
.wrap{height:calc(100svh / var(--page-zoom));padding:14px 28px 16px;display:flex;flex-direction:column;overflow:hidden}
.crumb{margin-bottom:10px}
.pdp{flex:1;min-height:0;gap:34px;align-items:stretch}
/* left column: stack tabs + active media region (fills) + selectors */
.stage{position:static;min-height:0;display:flex;flex-direction:column;overflow:hidden}
#roomFrameWrap,#photos,#previewFrame{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}
.room-frame{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
.room{width:100%;height:100%;aspect-ratio:auto;max-height:none;max-width:100%}
.preview-frame{padding:16px}
.preview{max-height:56svh}
.photos .hero-img{aspect-ratio:auto;height:100%;width:auto;max-width:100%;max-height:60svh;margin:0 auto}
/* right column: scrolls internally only as a last resort on very short screens */
.panel{min-height:0;overflow-y:auto;max-width:none;padding-right:6px}
h1{font-size:27px;margin:4px 0 4px}
.colors{margin:8px 0 10px}
.desc{font-size:13px;line-height:1.45;margin:0 0 10px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
.calc{padding:13px}
.calc h2{margin-bottom:9px}
.unit-tog{margin-bottom:10px}
.sliders{gap:10px}
.hint{display:none}
.note{display:none}
.srow .shead{margin-bottom:5px}
.srow .sval{font-size:18px}
.presets{gap:6px;margin:10px 0 2px}
.presets button{padding:5px 9px;font-size:11.5px}
.hint{font-size:11px;margin:6px 2px 0}
.readout{margin:10px 0 2px;display:grid;grid-template-columns:1fr 1fr;column-gap:20px}
.readout div{padding:5px 2px;font-size:12px}
.price-block{margin:10px 0 2px}
.price{font-size:31px}
.perm2{margin-bottom:10px}
.cta{padding:12px}
.cta.primary{margin-bottom:8px}
.note{margin-top:8px;font-size:11px;line-height:1.4}
/* specs as a compact disclosure so it's available without pushing the fold */
.specs-wrap{margin-top:14px;border-top:1px solid var(--line);padding-top:10px}
.specs-wrap>summary{font-family:var(--serif);font-size:20px;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between}
.specs-wrap>summary::-webkit-details-marker{display:none}
.specs-wrap>summary::after{content:'+';color:var(--muted);font-family:var(--sans);font-size:20px;font-weight:300}
.specs-wrap[open]>summary::after{content:'\2013'}
.specs-wrap .specs{margin-top:6px}
/* ===== user-resizable split — drag the corner hamburger grip to grow/shrink the image ===== */
.pdp{position:relative}
.stage{position:relative}
.grip{position:absolute;bottom:8px;width:28px;height:22px;border-radius:6px;background:var(--paper);
border:1px solid var(--line);box-shadow:0 1px 5px rgba(0,0,0,.16);cursor:ew-resize;
display:flex;flex-direction:column;justify-content:center;align-items:center;gap:3px;
z-index:6;touch-action:none;opacity:.5;transition:opacity .15s,border-color .15s;user-select:none}
.grip:hover,.grip.active{opacity:1;border-color:var(--accent)}
.grip span{display:block;width:15px;height:1.5px;border-radius:2px;background:var(--muted)}
.grip.active span{background:var(--accent-ink)}
.grip.left{right:8px} /* bottom-right corner of the image (left) panel */
.grip.right{right:0} /* bottom-right corner of the page = right panel corner */
@media(max-width:920px){.grip{display:none}}
/* on phones, stop locking the viewport — let it stack and scroll normally */
@media(max-width:920px){
body{overflow:auto}
.wrap{height:auto;overflow:visible;padding-bottom:60px}
.pdp{display:block}
.panel{overflow:visible;max-width:470px}
.room{max-height:none;height:auto;width:100%}
.photos .hero-img{height:auto;width:100%;max-height:none}
}
</style>
</head>
<body>
<div class="wrap">
<nav class="crumb"><a href="https://hollywoodwallcoverings.com">Hollywood Wallcoverings</a> / <a href="/">Custom Creator</a> / <a href="#">Made to Measure</a> / <span id="bcTitle">—</span></nav>
<div class="pdp" id="pdp">
<div class="stage">
<div class="tabs">
<button id="tabRoom" class="on">In a Room</button>
<button id="tabPhotos">Photos</button>
<button id="tabPreview">Wall Preview</button>
</div>
<div id="roomFrameWrap">
<div class="room-frame"><div class="room" id="room">
<div class="room-wall" id="roomWall"><div class="room-mural" id="roomMural"><div class="rz" id="rzHandle"></div></div></div>
<img class="room-fg" id="roomFg2" alt="">
<div class="room-cap" id="roomCap">—</div>
</div></div>
<div class="room-sel" id="roomSel"></div>
<input type="file" id="roomUpload" accept="image/*" capture="environment" style="display:none">
<div class="cust-ctrl hide" id="custCtrl">
<span>Drag to position · corner handle to resize</span>
<label>Opacity <input type="range" id="muralOpacity" min="0.4" max="1" step="0.05" value="1"></label>
<button id="reuploadBtn">Change photo</button>
</div>
<div class="room-pick" id="roomPick"></div>
</div>
<div class="photos hide" id="photos"><img class="hero-img" id="hero" alt=""><div class="thumbs" id="thumbs"></div></div>
<div class="preview-frame hide" id="previewFrame"><div class="preview" id="preview"></div></div>
<div class="ov hide" id="ovBar">
<button class="on" data-ov="seams">Roll seams</button>
<button class="on" data-ov="nums">Roll numbering</button>
<button class="on" data-ov="trim">Trim area</button>
<button data-ov="flip">Flip</button>
</div>
</div>
<div class="panel">
<div class="vendor" id="vendor">Made to Measure · Custom Mural</div>
<h1 id="title">Loading…</h1>
<div class="colors" id="colors"></div>
<div class="desc" id="desc"></div>
<div class="calc">
<h2>Configure Your Mural</h2>
<div class="unit-tog" id="unitTog"><button data-u="ft" class="on">Feet</button><button data-u="in">Inches</button></div>
<div class="sliders">
<div class="srow"><div class="shead"><label>Wall width</label><span class="sval" id="wv">13′ 1″</span></div><input id="w" type="range" min="20" max="470" step="1" value="157"></div>
<div class="srow"><div class="shead"><label>Wall height</label><span class="sval" id="hv">9′ 2″</span></div><input id="h" type="range" min="20" max="470" step="1" value="110"></div>
</div>
<div class="presets" id="presets"></div>
<p class="hint">Tip: add ~4″ to your true wall size for installation trim. <span id="trimEg"></span></p>
<dl class="readout" id="readout"></dl>
<div class="price-block"><span class="lbl">Your mural</span><span class="price" id="price">—</span></div>
<div class="perm2" id="perm2"></div>
<button class="cta primary" id="addMural">Add Custom Mural to Cart</button>
<button class="cta ghost" id="addSample">Order a Sample — $4.25</button>
<p class="note">Pick a pattern and room, set your wall size, and see it scale on a real wall live. Printed to your exact size on premium non-woven in <span id="dropW">20</span>″ drops; trade pricing at checkout.</p>
</div>
<details class="specs-wrap">
<summary>Specifications</summary>
<dl class="specs" id="specs"></dl>
</details>
</div>
</div>
</div>
<script>
const CM_PER_IN=2.54, ROLL_W_CM=50, TRIM_CM=5;
// Photoreal room templates: real photo (floor/sides/ceiling) + furniture cutout overlay (occlusion)
// + the flat feature-wall rectangle (inset %, front-facing) it represents in cm.
const ROOMS=[
{id:'living', name:'Living Room', bg:'/CustomCreator/rooms/living.jpg', fg:'/CustomCreator/rooms/living-fg.png', wall:{l:2,t:4,r:8,b:17}, realW:430, realH:250},
{id:'bedroom',name:'Bedroom', bg:'/CustomCreator/rooms/bedroom.jpg', fg:'/CustomCreator/rooms/bedroom-fg.png', wall:{l:1,t:3,r:1,b:20}, realW:440, realH:250},
{id:'dining', name:'Dining Room', bg:'/CustomCreator/rooms/dining.jpg', fg:'/CustomCreator/rooms/dining-fg.png', wall:{l:4,t:13,r:13,b:18},realW:430, realH:225},
{id:'office', name:'Office', bg:'/CustomCreator/rooms/office.jpg', fg:'/CustomCreator/rooms/office-fg.png', wall:{l:5,t:3,r:1,b:22}, realW:440, realH:250},
{id:'nursery',name:'Nursery', bg:'/CustomCreator/rooms/nursery.jpg', fg:'/CustomCreator/rooms/nursery-fg.png', wall:{l:1,t:2,r:1,b:20}, realW:440, realH:255},
{id:'hallway',name:'Entryway', bg:'/CustomCreator/rooms/hallway.jpg', fg:'/CustomCreator/rooms/hallway-fg.png', wall:{l:18,t:13,r:18,b:17},realW:380,realH:240},
{id:'lobby', name:'Lobby', bg:'/CustomCreator/rooms/lobby.jpg', fg:'/CustomCreator/rooms/lobby-fg.png', wall:{l:6,t:4,r:12,b:28}, realW:600, realH:300},
];
let RATE=83.38;
let state={unit:'ft',product:null,view:'room',flip:false,ov:{seams:true,nums:true,trim:true},roomIdx:0,list:[],
custom:false,userRoom:null,cbox:{l:22,t:12,w:56,h:64},opacity:1};
const $=id=>document.getElementById(id);
const fmt=n=>'$'+n.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
const val=id=>parseFloat($(id).value)||0;
// sliders always hold INCHES; the unit toggle only changes how lengths read (feet-then-inches vs inches). No cm anywhere.
const toCm=v=>v*CM_PER_IN;
const ftIn=inch=>{let f=Math.floor(inch/12),i=Math.round(inch-f*12);if(i===12){f++;i=0;}return f+'′ '+i+'″';};
const fmtLen=inch=>state.unit==='ft'?ftIn(inch):(Math.round(inch)+'″');
const cmToLen=cm=>fmtLen(cm/CM_PER_IN);
const muralUrl=()=>state.product?(state.product.flatImage||(state.product.images[0]&&state.product.images[0].url)||''):'';
function metrics(){
const w=toCm(val('w')),h=toCm(val('h'));
const netA=(w/100)*(h/100), pw=w+2*TRIM_CM, ph=h+2*TRIM_CM, printA=(pw/100)*(ph/100);
const drops=w>0?Math.ceil(pw/ROLL_W_CM):0;
return {w,h,pw,ph,netA,printA,drops,price:netA>0?RATE*netA:0};
}
function updateSval(){ $('wv').textContent=fmtLen(val('w')); $('hv').textContent=fmtLen(val('h')); }
function relabelPresets(){document.querySelectorAll('#presets button').forEach(b=>{const w=b._w,h=b._h;if(w==null)return;b.textContent=state.unit==='ft'?(Math.round(w/12)+'×'+Math.round(h/12)+' ft'):(w+'×'+h+'″');});}
function recalc(){
updateSval();
const m=metrics();
$('readout').innerHTML='';
if(m.netA>0)[['Net wall area',(m.netA*10.7639).toFixed(1)+' sq ft'],
['Printed area (incl. trim)',(m.printA*10.7639).toFixed(1)+' sq ft'],['Drops / panels',m.drops+' panels'],
['Trim margin','2″ each edge']].forEach(([k,v])=>{const d=document.createElement('div');d.innerHTML='<dt>'+k+'</dt><dd>'+v+'</dd>';$('readout').appendChild(d);});
if(state.product&&state.product.bespoke){
$('price').innerHTML=m.netA>0?fmt(m.price)+' <small>USD</small>':'—';
$('perm2').textContent=m.netA>0?'Bespoke · '+fmt(RATE)+'/m² · '+cmToLen(m.w)+' × '+cmToLen(m.h)+' · inquire to order':'';
} else {
$('price').innerHTML=m.netA>0?fmt(m.price)+' <small>USD</small>':'—';
$('perm2').textContent=m.netA>0?fmt(RATE)+'/m² · '+cmToLen(m.w)+' × '+cmToLen(m.h)+' · '+m.drops+' drops':'';
}
renderPreview(m); renderRoom(m);
}
function renderPreview(m){
if(state.view!=='preview'||!m||m.netA<=0)return;
const box=$('preview'),frame=$('previewFrame');
const maxW=frame.clientWidth-44,maxH=Math.min(window.innerHeight*0.6,540),ar=m.pw/m.ph;
let dw=maxW,dh=dw/ar; if(dh>maxH){dh=maxH;dw=dh*ar;}
box.style.width=dw+'px';box.style.height=dh+'px';
box.style.backgroundImage='url("'+muralUrl()+'")';box.style.transform=state.flip?'scaleX(-1)':'none';
box.innerHTML='';
if(state.ov.trim){const tx=(TRIM_CM/m.pw)*100,ty=(TRIM_CM/m.ph)*100;const t=document.createElement('div');t.className='trim';t.style.left=tx+'%';t.style.top=ty+'%';t.style.right=tx+'%';t.style.bottom=ty+'%';box.appendChild(t);
const lbl=document.createElement('div');lbl.className='trim-lbl';lbl.style.left=tx+'%';lbl.style.top='calc('+ty+'% - 14px)';lbl.textContent='wall '+cmToLen(m.w)+'×'+cmToLen(m.h);box.appendChild(lbl);}
if(state.ov.seams||state.ov.nums)for(let i=0;i<m.drops;i++){const lp=((i*ROLL_W_CM)/m.pw)*100;
if(state.ov.seams&&i>0){const s=document.createElement('div');s.className='seam';s.style.left=lp+'%';box.appendChild(s);}
if(state.ov.nums){const cx=(((i+0.5)*ROLL_W_CM)/m.pw)*100;const n=document.createElement('div');n.className='dropnum';n.style.left=Math.min(cx,98)+'%';n.textContent=(i+1);box.appendChild(n);}}
}
function applyCustomMural(){
const mu=$('roomMural');
mu.style.left=state.cbox.l+'%'; mu.style.top=state.cbox.t+'%'; mu.style.right='auto';
mu.style.width=state.cbox.w+'%'; mu.style.height=state.cbox.h+'%';
mu.style.transform=state.flip?'scaleX(-1)':'none'; mu.style.opacity=state.opacity;
mu.style.backgroundImage='url("'+muralUrl()+'")';
}
function renderRoom(m){
if(state.view!=='room'||!m||m.netA<=0)return;
const mu=$('roomMural');
if(state.custom){
$('room').style.backgroundImage='url("'+state.userRoom+'")';
$('roomFg2').style.display='none';
const wall=$('roomWall'); wall.style.left='0';wall.style.top='0';wall.style.right='0';wall.style.bottom='0';wall.style.overflow='visible';
mu.classList.add('custom'); applyCustomMural();
$('roomCap').textContent='Your room · drag & resize the mural onto your wall';
return;
}
const room=ROOMS[state.roomIdx];
$('roomFg2').style.display=''; mu.classList.remove('custom'); mu.style.opacity=1; mu.style.left='50%'; mu.style.top='auto';
$('room').style.backgroundImage='url("'+room.bg+'")';
$('roomFg2').src=room.fg;
const wall=$('roomWall'); wall.style.overflow='hidden';
wall.style.left=room.wall.l+'%'; wall.style.top=room.wall.t+'%'; wall.style.right=room.wall.r+'%'; wall.style.bottom=room.wall.b+'%';
const wPct=Math.max(8,(m.w/room.realW)*100), hPct=Math.max(8,(m.h/room.realH)*100);
mu.style.width=wPct+'%'; mu.style.height=Math.min(hPct,135)+'%';
mu.style.backgroundImage='url("'+muralUrl()+'")';
mu.style.transform='translateX(-50%)'+(state.flip?' scaleX(-1)':'');
$('roomCap').textContent=cmToLen(m.w)+' × '+cmToLen(m.h)+' · shown to scale in the '+room.name.toLowerCase();
}
// pointer drag + resize for the uploaded-room mural (works mouse + touch on desktop/mobile/iPad)
let _drag=null;
function muralDown(e){
if(!state.custom)return;
const isRz=e.target.id==='rzHandle';
_drag={mode:isRz?'resize':'move',sx:e.clientX,sy:e.clientY,box:{...state.cbox}};
try{$('roomMural').setPointerCapture(e.pointerId);}catch(_){}
e.preventDefault();
}
function muralMove(e){
if(!_drag)return;
const r=$('room').getBoundingClientRect();
const dx=(e.clientX-_drag.sx)/r.width*100, dy=(e.clientY-_drag.sy)/r.height*100;
if(_drag.mode==='move'){ state.cbox.l=Math.max(-30,Math.min(95,_drag.box.l+dx)); state.cbox.t=Math.max(-30,Math.min(95,_drag.box.t+dy)); }
else { state.cbox.w=Math.max(8,Math.min(160,_drag.box.w+dx)); state.cbox.h=Math.max(8,Math.min(180,_drag.box.h+dy)); }
applyCustomMural(); e.preventDefault();
}
function muralUp(){ _drag=null; }
function setView(v){
state.view=v;
$('tabRoom').classList.toggle('on',v==='room'); $('tabPhotos').classList.toggle('on',v==='photos'); $('tabPreview').classList.toggle('on',v==='preview');
$('roomFrameWrap').classList.toggle('hide',v!=='room');
$('photos').classList.toggle('hide',v!=='photos');
$('previewFrame').classList.toggle('hide',v!=='preview'); $('ovBar').classList.toggle('hide',v!=='preview');
recalc();
}
function render(p){
state.product=p; RATE=p.rate||83.38;
$('title').textContent=p.pattern; $('bcTitle').textContent=p.pattern;
$('vendor').textContent=(p.vendor||'Made to Measure')+' · Custom Mural';
const imgs=p.images.length?p.images:[{url:'',alt:''}];
$('hero').src=imgs[0].url; $('hero').alt=imgs[0].alt;
const tw=$('thumbs'); tw.innerHTML='';
imgs.forEach((im,k)=>{const t=new Image();t.src=im.url;t.alt=im.alt;t.className=k===0?'active':'';t.onclick=()=>{$('hero').src=im.url;[...tw.children].forEach(c=>c.classList.remove('active'));t.classList.add('active');};tw.appendChild(t);});
const cw=$('colors'); cw.innerHTML='';
(p.colors||[]).forEach(c=>{const s=document.createElement('span');s.className='chip';s.innerHTML='<i style="background:'+c.hex+'"></i>'+(c.name||c.hex)+(c.pct?' · '+c.pct+'%':'');cw.appendChild(s);});
$('desc').innerHTML=p.descriptionHtml||'';
const dl=$('specs'); dl.innerHTML='';
(p.specs||[]).forEach(s=>{const d=document.createElement('div');d.innerHTML='<dt>'+s.label+'</dt><dd>'+s.value+'</dd>';dl.appendChild(d);});
if(p.bespoke){ $('addMural').textContent='Request a Quote →'; $('addSample').style.display='none'; $('vendor').textContent='Bespoke Mural'; }
else { $('addMural').textContent='Add Custom Mural to Cart'; $('addSample').style.display=''; $('addSample').textContent='Order a Sample — '+fmt(parseFloat(p.samplePrice||4.25)); }
[...$('roomPick').children].forEach(b=>b.classList.toggle('on',b.dataset.h===(p.sku||'').toLowerCase()));
recalc();
}
async function load(handle){
$('pdp').classList.add('loading');
try{
const r=await fetch('/CustomCreator/api/product'+(handle?('?handle='+encodeURIComponent(handle)):''));
const p=await r.json(); if(p.error)throw new Error(p.error);
render(p);
history.replaceState(null,'',location.pathname+'?handle='+p.sku.toLowerCase());
}catch(e){ console.error(e); location.replace('/CustomCreator'); return; } // unknown/retired design → land on the gallery, never a dead end
$('pdp').classList.remove('loading');
}
async function postCart(mode){
const p=state.product; if(!p)return;
if(p.bespoke){ const wcm=toCm(val('w')), hcm=toCm(val('h')), area=((wcm/100)*(hcm/100)*10.7639).toFixed(1), sz=ftIn(val('w'))+' × '+ftIn(val('h'));
window.location.href='mailto:info@hollywoodwallcoverings.com?subject='+encodeURIComponent('Mural Quote: '+p.pattern)+'&body='+encodeURIComponent('I would like a quote for the '+p.pattern+' mural at '+sz+' (approx '+area+' sq ft).'); return; }
const btn=$(mode==='sample'?'addSample':'addMural'); const orig=btn.textContent; btn.disabled=true; btn.textContent='Creating checkout…';
try{
const r=await fetch('/CustomCreator/api/mural-cart',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode,sku:p.sku,pattern:p.pattern,unit:'in',width:val('w'),height:val('h')})});
const j=await r.json();
if(j.invoiceUrl){window.location.href=j.invoiceUrl;return;}
alert(j.scopeHint?'Checkout not yet enabled — token needs write_draft_orders.':'Could not create checkout: '+(j.error||'unknown'));
}catch(e){alert('Network error: '+e.message);}
btn.disabled=false; btn.textContent=orig;
}
// ===== resizable split: corner hamburger grips on both panels adjust the image size =====
const DEFAULT_SPLIT=0.59; // matches the original 1.18fr : .82fr column ratio
let split=Math.max(0.32,Math.min(0.80,parseFloat(localStorage.getItem('muralSplit'))||DEFAULT_SPLIT));
function applySplit(){ // persists via localStorage; two fr values that always sum to 2 (like 1.18+.82)
$('pdp').style.gridTemplateColumns=(split*2).toFixed(3)+'fr '+((1-split)*2).toFixed(3)+'fr';
}
function makeGrip(cls,host){
const g=document.createElement('div'); g.className='grip '+cls;
g.title='Drag to resize the image · double-click to reset';
g.innerHTML='<span></span><span></span><span></span>';
let dragging=false;
g.addEventListener('pointerdown',e=>{dragging=true;g.classList.add('active');try{g.setPointerCapture(e.pointerId);}catch(_){}e.preventDefault();});
g.addEventListener('pointermove',e=>{ if(!dragging)return;
const r=$('pdp').getBoundingClientRect();
split=Math.max(0.32,Math.min(0.80,(e.clientX-r.left)/r.width));
applySplit(); renderPreview(metrics()); renderRoom(metrics()); e.preventDefault(); });
const end=()=>{ if(!dragging)return; dragging=false; g.classList.remove('active'); localStorage.setItem('muralSplit',split.toFixed(4)); };
g.addEventListener('pointerup',end); g.addEventListener('pointercancel',end);
g.addEventListener('dblclick',()=>{ split=DEFAULT_SPLIT; applySplit(); localStorage.setItem('muralSplit',split.toFixed(4)); renderPreview(metrics()); renderRoom(metrics()); });
host.appendChild(g);
}
function setupGrips(){ applySplit(); makeGrip('left',document.querySelector('.stage')); makeGrip('right',$('pdp')); }
function init(){
$('dropW').textContent=ROLL_W_CM;
// room selector (predefined rooms + upload-your-own)
const rs=$('roomSel');
const clearSel=()=>[...rs.children].forEach(c=>c.classList.remove('on'));
ROOMS.forEach((rm,i)=>{const b=document.createElement('button');b.textContent=rm.name;b.className=i===0?'on':'';
b.onclick=()=>{state.custom=false;state.roomIdx=i;clearSel();b.classList.add('on');$('custCtrl').classList.add('hide');renderRoom(metrics());};rs.appendChild(b);});
const up=document.createElement('button'); up.textContent='+ Your Room'; up.className='upload'; up.onclick=()=>$('roomUpload').click(); rs.appendChild(up);
// upload handler — file input is camera-capable on mobile/iPad, gallery on desktop
$('roomUpload').onchange=e=>{const f=e.target.files&&e.target.files[0]; if(!f)return;
const rd=new FileReader(); rd.onload=()=>{ state.userRoom=rd.result; state.custom=true; state.cbox={l:22,t:12,w:56,h:64}; state.opacity=1; $('muralOpacity').value=1;
clearSel(); up.classList.add('on'); $('custCtrl').classList.remove('hide'); if(state.view!=='room')setView('room'); renderRoom(metrics()); };
rd.readAsDataURL(f); e.target.value=''; };
$('reuploadBtn').onclick=()=>$('roomUpload').click();
$('muralOpacity').oninput=()=>{state.opacity=parseFloat($('muralOpacity').value);applyCustomMural();};
// drag + resize (pointer events = mouse + touch)
const mu=$('roomMural');
mu.addEventListener('pointerdown',muralDown); mu.addEventListener('pointermove',muralMove); mu.addEventListener('pointerup',muralUp); mu.addEventListener('pointercancel',muralUp);
document.querySelectorAll('#unitTog button').forEach(btn=>btn.onclick=()=>{
const u=btn.dataset.u; if(u===state.unit)return;
// display-only toggle — the slider already holds inches, so just re-render the labels
state.unit=u; document.querySelectorAll('#unitTog button').forEach(b=>b.classList.toggle('on',b===btn)); relabelPresets(); recalc();
});
$('trimEg').textContent='[e.g. a 10′×8′ wall → add a few inches each side for trim]';
[[120,96],[144,108],[168,108],[96,96],[192,108],[144,120]].forEach(([w,h])=>{const b=document.createElement('button');b._w=w;b._h=h;b.onclick=()=>{$('w').value=w;$('h').value=h;recalc();};$('presets').appendChild(b);});
relabelPresets();
document.querySelectorAll('#ovBar button').forEach(b=>b.onclick=()=>{const k=b.dataset.ov;if(k==='flip'){state.flip=!state.flip;b.classList.toggle('on',state.flip);}else{state.ov[k]=!state.ov[k];b.classList.toggle('on',state.ov[k]);}renderPreview(metrics());renderRoom(metrics());});
$('tabRoom').onclick=()=>setView('room'); $('tabPhotos').onclick=()=>setView('photos'); $('tabPreview').onclick=()=>setView('preview');
$('w').oninput=recalc; $('h').oninput=recalc;
$('addMural').onclick=()=>postCart('mural'); $('addSample').onclick=()=>postCart('sample');
window.addEventListener('resize',()=>{renderPreview(metrics());renderRoom(metrics());});
setupGrips();
const params=new URLSearchParams(location.search), start=params.get('handle');
fetch('/CustomCreator/api/products').then(r=>r.json()).then(list=>{
state.list=list;
const pk=$('roomPick'); pk.innerHTML='';
list.slice(0,18).forEach(it=>{const b=document.createElement('button');b.dataset.h=it.handle;b.title=it.title;if(it.image)b.style.backgroundImage='url("'+it.image+'")';b.onclick=()=>load(it.handle);pk.appendChild(b);});
load(start||(list[0]&&list[0].handle));
}).catch(()=>load(start));
}
init();
</script>
</body>
</html>