← back to Dw Photo Capture
public/simple-instant.html
454 lines
<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — see index.html for the full rationale.
// Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="DW Instant">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#efe2c4">
<link rel="apple-touch-icon" sizes="180x180" href="/icon-180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<link rel="manifest" href="/manifest.webmanifest">
<title>DW Instant Film</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&display=swap" rel="stylesheet">
<style>
:root{
--paper:#f7efdd; --paper-deep:#efe1bf; --white:#fffdf6;
--ink:#382d1e; --muted:#8d7c5c; --line:#e3d3ab;
--gold:#c8a24a; --gold-deep:#a67f2e; --gold-soft:#e7c878;
--shadow:0 20px 44px -14px rgba(56,45,30,.38), 0 4px 12px rgba(56,45,30,.16);
--radius:26px;
--flash-ms:150ms; --develop-ms:1100ms;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
--paper:#241d12; --paper-deep:#1a1509; --white:#f7efdd;
--ink:#f2e8d3; --muted:#b7a37a; --line:#4a3d24;
}
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%}
body{
margin:0; min-height:100vh; min-height:100dvh;
background:
radial-gradient(1200px 800px at 15% -10%, var(--paper-deep), transparent 60%),
radial-gradient(1000px 700px at 110% 110%, var(--paper-deep), transparent 55%),
var(--paper);
color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
display:flex; flex-direction:column; align-items:center; justify-content:flex-start;
padding:max(18px,env(safe-area-inset-top)) 18px max(24px,env(safe-area-inset-bottom));
overflow-x:hidden;
}
.brand{display:flex;align-items:baseline;gap:8px;margin:2px 130px 4px 0;user-select:none;max-width:100%;overflow:hidden}
.brand b{font-size:clamp(11px,3.6vw,15px);letter-spacing:.1em;color:var(--ink);white-space:nowrap}
.brand b .short{display:none}
.brand .script{font-family:'Caveat',cursive;font-size:22px;color:var(--gold-deep);font-weight:700;flex:none}
@media (max-width:400px){
.brand{ margin-right:112px; }
.brand b .full{display:none} .brand b .short{display:inline}
}
.sesschip{
position:fixed; top:max(14px,env(safe-area-inset-top)); right:14px; z-index:5;
background:var(--white); border:1px solid var(--line); color:var(--muted);
font-size:12px; font-weight:600; padding:6px 11px; border-radius:99px; box-shadow:var(--shadow);
}
.sesschip b{color:var(--gold-deep)}
/* ===== VIEWFINDER (idle / live camera) ===== */
.stage{ width:min(94vw,480px); display:flex; flex-direction:column; align-items:center; gap:22px; margin-top:6px; }
/* short-viewport / landscape iPad — the 3:4 frame at full width would clip the shutter below the
fold, so cap the frame's width (hence height, via aspect-ratio) to what the viewport can hold. */
@media (max-height:840px) and (min-width:640px){
.stage,.card-stage{ width:min(90vw,230px); }
.brand{ margin-bottom:0; }
.field{ margin-bottom:6px; }
.field input{ padding:8px 10px; font-size:14px; }
.chin{ padding:8px 4px 10px; }
.chin-row{ min-height:20px; font-size:18px; }
.save-row{ padding-bottom:8px; }
body{ justify-content:center; padding-top:max(10px,env(safe-area-inset-top)); }
.stage{ gap:14px; }
.card-stage{ gap:10px; }
}
.viewfinder{
position:relative; width:100%; aspect-ratio:3/4; border-radius:var(--radius);
background:var(--white); padding:14px 14px 34px;
box-shadow:var(--shadow); border:1px solid var(--line);
}
.vf-window{ position:relative; width:100%; height:100%; border-radius:16px; overflow:hidden; background:#111; }
.vf-window video{ width:100%; height:100%; object-fit:cover; display:block; transform:scaleX(1); }
.vf-corner{ position:absolute; width:22px; height:22px; border-color:rgba(255,255,255,.55); pointer-events:none; }
.vf-corner.tl{ top:8px; left:8px; border-top:2px solid; border-left:2px solid; border-radius:4px 0 0 0; }
.vf-corner.tr{ top:8px; right:8px; border-top:2px solid; border-right:2px solid; border-radius:0 4px 0 0; }
.vf-corner.bl{ bottom:8px; left:8px; border-bottom:2px solid; border-left:2px solid; border-radius:0 0 0 4px; }
.vf-corner.br{ bottom:8px; right:8px; border-bottom:2px solid; border-right:2px solid; border-radius:0 0 4px 0; }
.flip-btn{
position:absolute; top:22px; right:22px; z-index:3; width:38px;height:38px;border-radius:50%;
background:rgba(20,16,10,.42); color:#fff; border:1px solid rgba(255,255,255,.35);
font-size:17px; display:flex;align-items:center;justify-content:center; cursor:pointer;
}
.vf-hint{
position:absolute; left:0; right:0; bottom:10px; text-align:center; font-size:12px;
color:rgba(255,255,255,.85); text-shadow:0 1px 3px rgba(0,0,0,.6); pointer-events:none; letter-spacing:.02em;
}
.shutter{
width:78px; height:78px; border-radius:50%; border:none; cursor:pointer; padding:0;
background:radial-gradient(circle at 34% 30%, var(--gold-soft), var(--gold) 55%, var(--gold-deep) 100%);
box-shadow:0 10px 22px -6px rgba(166,127,46,.55), inset 0 0 0 5px var(--white), inset 0 0 0 7px var(--gold);
display:flex; align-items:center; justify-content:center; transition:transform .12s ease;
}
.shutter:active{ transform:scale(.9); }
.shutter .dot{ width:16px;height:16px;border-radius:50%;background:var(--white); box-shadow:0 0 0 2px rgba(0,0,0,.06) inset; }
.shutter[disabled]{ opacity:.5; pointer-events:none; }
.fullres-btn{ margin-top:-8px; border:1px solid var(--line); background:var(--white); color:var(--gold-deep);
font:600 13px/1 -apple-system,system-ui,sans-serif; padding:9px 16px; border-radius:99px; cursor:pointer; }
/* ===== FLASH ===== */
.flash-overlay{ position:fixed; inset:0; background:#fff; opacity:0; pointer-events:none; z-index:40; }
.flash-overlay.go{ animation:flashpop var(--flash-ms) ease-out; }
@keyframes flashpop{ 0%{opacity:0} 18%{opacity:.92} 100%{opacity:0} }
/* ===== INSTANT CARD (developing / result) ===== */
.card-stage{ width:min(94vw,480px); display:flex; flex-direction:column; align-items:center; gap:18px; }
.instant-card{
width:100%; background:var(--white); border-radius:var(--radius); box-shadow:var(--shadow);
padding:14px 14px 0; border:1px solid var(--line);
animation:cardpop .38s cubic-bezier(.2,.9,.3,1.2);
}
@keyframes cardpop{ from{ transform:scale(.92) translateY(10px); opacity:0 } to{ transform:none; opacity:1 } }
.photo-well{ position:relative; width:100%; aspect-ratio:3/4; border-radius:16px; overflow:hidden; background:#1c1712; }
.photo-well img{
width:100%; height:100%; object-fit:cover; display:block;
filter:blur(16px) saturate(.35) brightness(1.18); opacity:.82;
transition:filter var(--develop-ms) cubic-bezier(.16,.7,.24,1), opacity var(--develop-ms) ease-out;
}
.photo-well img.developed{ filter:blur(0) saturate(1) brightness(1); opacity:1; }
.sheen{
position:absolute; inset:0; pointer-events:none;
background:linear-gradient(115deg, transparent 35%, rgba(255,255,255,.55) 48%, transparent 62%);
transform:translateX(-120%); opacity:0;
}
.sheen.go{ animation:sheensweep var(--develop-ms) ease-out forwards; }
@keyframes sheensweep{ 0%{transform:translateX(-120%);opacity:.9} 70%{opacity:.5} 100%{transform:translateX(120%);opacity:0} }
.chin{ padding:16px 6px 18px; text-align:center; }
.chin-row{ font-family:'Caveat',cursive; font-size:22px; color:var(--muted); display:flex; align-items:center; justify-content:center; gap:8px; min-height:30px; }
.dot-flicker{ width:8px;height:8px;border-radius:50%;background:var(--gold); animation:flicker 1.1s ease-in-out infinite; }
@keyframes flicker{ 0%,100%{opacity:.25} 50%{opacity:1} }
.chin-fields{ text-align:left; }
.field{ margin-bottom:10px; }
.field label{ display:block; font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }
.field input{
width:100%; font-size:16px; padding:11px 12px; border-radius:10px; border:1.5px solid var(--line);
background:var(--paper); color:var(--ink); font-family:inherit;
}
.field input:focus{ outline:none; border-color:var(--gold); background:var(--white); }
.field.optional input{ font-size:14px; padding:9px 11px; }
.tiny-meta{ font-size:11px; color:var(--muted); text-align:center; margin:2px 0 10px; min-height:14px; }
.tiny-meta b{ color:var(--gold-deep) }
.save-row{ display:flex; gap:10px; padding-bottom:16px; }
.btn{ flex:1; border:none; border-radius:14px; padding:15px 10px; font-size:15px; font-weight:700; cursor:pointer; text-align:center; font-family:inherit; }
.btn-outline{ background:var(--white); color:var(--ink); border:1.5px solid var(--line); flex:0.85; }
.btn-outline:active{ background:var(--paper); }
.btn-gold{ background:linear-gradient(180deg, var(--gold-soft), var(--gold)); color:#332506; flex:1.3; box-shadow:0 8px 18px -8px rgba(166,127,46,.6); transition:opacity .15s,transform .12s; }
.btn-gold:active:not([disabled]){ transform:scale(.98); }
.btn-gold[disabled]{ opacity:.5; cursor:default; box-shadow:none; }
.toast{
position:fixed; left:50%; bottom:max(22px,env(safe-area-inset-bottom)); transform:translateX(-50%) translateY(8px);
background:#241d12; color:#f7efdd; padding:12px 20px; border-radius:14px; font-size:14px; font-weight:600;
box-shadow:var(--shadow); z-index:50; opacity:0; pointer-events:none; transition:opacity .25s, transform .25s;
}
.toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
[hidden]{ display:none !important; }
</style>
</head>
<body>
<div class="brand"><b><span class="full">DESIGNER WALLCOVERINGS</span><span class="short">DW</span></b><span class="script">Instant</span></div>
<div class="sesschip">📸 <b id="sessCount">0</b> saved this session</div>
<!-- STATE: live viewfinder -->
<div class="stage" id="stage">
<div class="viewfinder">
<div class="vf-window">
<video id="cam" autoplay playsinline muted></video>
<div class="vf-corner tl"></div><div class="vf-corner tr"></div>
<div class="vf-corner bl"></div><div class="vf-corner br"></div>
<button class="flip-btn" id="flipBtn" title="Flip camera" aria-label="Flip camera">⟲</button>
<div class="vf-hint" id="vfHint">frame the label & tap to shoot</div>
</div>
</div>
<button class="shutter" id="shutterBtn" aria-label="Take photo"><span class="dot"></span></button>
<button class="fullres-btn" id="fullResBtn">📷 Full-res photo</button>
</div>
<!-- STATE: developing / result card -->
<div class="card-stage" id="cardStage" hidden>
<div class="instant-card">
<div class="photo-well">
<img id="shotImg" alt="Captured sample">
<div class="sheen" id="sheen"></div>
</div>
<div class="chin">
<div class="chin-row" id="developingRow"><span class="dot-flicker"></span> developing…</div>
<div class="chin-fields" id="chinFields" hidden>
<div class="field">
<label for="fVendor">Vendor</label>
<input id="fVendor" list="vendorList" placeholder="e.g. Kravet" autocomplete="off">
<datalist id="vendorList"></datalist>
</div>
<div class="field">
<label for="fMfr">Manufacturer # / SKU</label>
<input id="fMfr" placeholder="e.g. 34567-1116" autocomplete="off">
</div>
<div class="field optional">
<label for="fPattern">Pattern / Color <span style="text-transform:none;font-weight:400">(optional)</span></label>
<input id="fPattern" placeholder="e.g. Bermuda — Sand" autocomplete="off">
</div>
<div class="tiny-meta" id="tinyMeta"> </div>
<div class="save-row">
<button class="btn btn-outline" id="retakeBtn">↺ Retake</button>
<button class="btn btn-gold" id="saveBtn" disabled>⏳ Reading label…</button>
</div>
</div>
</div>
</div>
</div>
<div class="flash-overlay" id="flashOverlay"></div>
<div class="toast" id="toast"></div>
<script src="/js/field-clean.js"></script>
<script src="/js/native-photo.js"></script>
<script src="/js/capture-pipeline.js"></script>
<script src="/js/adjust-panel.js"></script>
<script>
(function(){
var $ = function(id){ return document.getElementById(id); };
var video = $('cam'), canvas = document.createElement('canvas');
var stage = $('stage'), cardStage = $('cardStage');
var shutterBtn = $('shutterBtn'), flipBtn = $('flipBtn');
var flashOverlay = $('flashOverlay'), sheen = $('sheen');
var shotImg = $('shotImg'), developingRow = $('developingRow'), chinFields = $('chinFields');
var fVendor = $('fVendor'), fMfr = $('fMfr'), fPattern = $('fPattern');
var tinyMeta = $('tinyMeta'), saveBtn = $('saveBtn'), retakeBtn = $('retakeBtn');
var toastEl = $('toast'), sessCountEl = $('sessCount'), vendorList = $('vendorList');
var FLASH_MS = 150, DEVELOP_MS = 1100; // purely cosmetic — never gates Save
var stream = null, facing = 'environment';
var busy = false, capturedDataUrl = null, extractDone = false, extractAbort = null;
var vendorFromExtract = false, sessCount = 0;
// Pre-shot adjustments (all 11) + auto-save once the label read fills vendor + mfr#.
var panel = AdjustPanel.create({ key:'dwSimpleTune', toggleParent: $('fullResBtn').parentNode, toggleClass:'fullres-btn' });
panel.attachPreview(video);
var autoFired = false, vendorRegistered = false;
function maybeAutoSave(){
// registry vendors only — an unmatched label vendor would mint a PROV- sku, so a human confirms it
if (autoFired || !panel.autoSave() || !extractDone || !vendorRegistered || chinFields.hidden || saveBtn.disabled) return;
if (!fVendor.value.trim() || !fMfr.value.trim()) return;
autoFired = true; saveBtn.click();
}
function toast(msg, ms){
toastEl.textContent = msg; toastEl.classList.add('show');
clearTimeout(toast._t); toast._t = setTimeout(function(){ toastEl.classList.remove('show'); }, ms || 2600);
}
// ---- camera: start immediately on load, never torn down between shots (retake is instant). ----
async function startCamera(f){
if (!(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia)){
$('vfHint').textContent = 'needs Safari/Chrome over HTTPS (or localhost)'; return;
}
try{
if (stream) stream.getTracks().forEach(function(t){ t.stop(); });
stream = await navigator.mediaDevices.getUserMedia({ video:{ facingMode:{ ideal:f }, width:{ ideal:4096 }, height:{ ideal:3072 } }, audio:false });
}catch(e){
try{ stream = await navigator.mediaDevices.getUserMedia({ video:true, audio:false }); }
catch(e2){ toast('Camera unavailable — check permissions.'); return; }
}
video.srcObject = stream;
try{ await video.play(); }catch(e){}
shutterBtn.disabled = false;
}
flipBtn.addEventListener('click', function(){
facing = facing === 'environment' ? 'user' : 'environment';
startCamera(facing);
});
// ---- vendor autocomplete: loaded in the background, never blocks camera/shutter. ----
fetch('/api/vendors-registry').then(function(r){ return r.json(); }).then(function(d){
var seen = {};
(d.vendors || []).forEach(function(v){
var name = v.vendor; if (!name || seen[name]) return; seen[name] = 1;
var o = document.createElement('option'); o.value = name; vendorList.appendChild(o);
});
}).catch(function(){});
function fmt(ms){ return ms < 1000 ? Math.round(ms) + 'ms' : (ms/1000).toFixed(1) + 's'; }
function updateSaveEnabled(){
var ok = fVendor.value.trim() && fMfr.value.trim();
saveBtn.disabled = !ok;
if (ok) saveBtn.textContent = '💾 Save';
else saveBtn.textContent = extractDone ? '✍️ Vendor + Mfr# needed' : '⏳ Reading label…';
}
fVendor.addEventListener('input', updateSaveEnabled);
fMfr.addEventListener('input', updateSaveEnabled);
function resetToLive(){
cardStage.hidden = true; stage.hidden = false;
shotImg.className = ''; shotImg.src = '';
developingRow.hidden = false; chinFields.hidden = true;
fVendor.value = ''; fMfr.value = ''; fPattern.value = '';
tinyMeta.innerHTML = ' ';
capturedDataUrl = null; extractDone = false; vendorFromExtract = false; busy = false;
saveBtn.disabled = true; saveBtn.textContent = '⏳ Reading label…';
shutterBtn.disabled = !stream; // re-arm the shutter — the camera stream is still live, no re-init needed
}
async function doCapture(native){
if (busy || (!stream && !native)) return;
busy = true; shutterBtn.disabled = true; autoFired = false; vendorRegistered = false;
if (native) {
capturedDataUrl = native.full;
} else {
var vw = video.videoWidth || 1280, vh = video.videoHeight || 960;
var maxDim = 2000, scale = Math.min(1, maxDim / Math.max(vw, vh));
canvas.width = Math.round(vw * scale); canvas.height = Math.round(vh * scale);
capturedDataUrl = panel.bake(video, maxDim); // pre-shot adjustments baked in
}
// ── SPEED: fire the REAL network call the instant we have pixels — before any
// flash/develop UI even starts. The cosmetic animation below runs on its own
// fixed clock; Save's enabled state is gated on this promise settling, never
// on the animation timers further down. TK-12162 "Instant Film" speed rule.
var t0 = performance.now();
extractAbort = new AbortController();
var extractPromise = fetch('/api/extract', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ dataUrl: (native && native.small) || capturedDataUrl }), signal: extractAbort.signal
}).then(function(r){ return r.json(); }).catch(function(e){
return { ok:false, err: e.name === 'AbortError' ? 'aborted' : (e.message || 'network') };
});
// ---- cosmetic: shutter-flash → develop reveal. Runs in parallel, blocks nothing. ----
flashOverlay.classList.remove('go'); void flashOverlay.offsetWidth; flashOverlay.classList.add('go');
setTimeout(function(){
stage.hidden = true; cardStage.hidden = false;
shotImg.src = capturedDataUrl; shotImg.className = '';
developingRow.hidden = false; chinFields.hidden = true;
sheen.classList.remove('go'); void sheen.offsetWidth; sheen.classList.add('go');
requestAnimationFrame(function(){ requestAnimationFrame(function(){ shotImg.classList.add('developed'); }); });
setTimeout(revealResult, DEVELOP_MS);
}, FLASH_MS);
extractPromise.then(function(data){
extractDone = true;
vendorRegistered = !!(data && data.vendor_registered === true);
var elapsed = performance.now() - t0;
var fields = (data && data.fields) || {};
if (!fVendor.value.trim() && (data.vendor_matched || fields.vendor)){
fVendor.value = data.vendor_matched || fields.vendor; vendorFromExtract = true;
}
if (!fMfr.value.trim() && fields.mfr_sku != null) fMfr.value = cleanField(fields.mfr_sku);
if (!fPattern.value.trim() && (fields.pattern_name || fields.color)){
fPattern.value = [fields.pattern_name, fields.color].filter(Boolean).join(' — ');
}
if (data && data.err) tinyMeta.innerHTML = '🔎 label read in <b>' + fmt(elapsed) + '</b> · no fields found, enter by hand';
else if (data && data.ok === false) tinyMeta.innerHTML = '🔎 checked in <b>' + fmt(elapsed) + '</b> · enter details by hand';
else tinyMeta.innerHTML = '🔎 label read in <b>' + fmt(elapsed) + '</b>';
updateSaveEnabled();
maybeAutoSave();
});
}
function revealResult(){
developingRow.hidden = true; chinFields.hidden = false;
updateSaveEnabled();
maybeAutoSave();
if (!extractDone) tinyMeta.innerHTML = '🔎 still reading the label…';
fVendor.focus({ preventScroll:true });
}
shutterBtn.addEventListener('click', function(){ doCapture(); }); // never pass the click event as `native`
// Full-res: the phone's own Camera app (real sensor photo); a 1600px copy feeds the label read.
$('fullResBtn').addEventListener('click', function(){
if (busy) return;
NativePhoto.pick().then(function(p){
if (!p) return;
panel.bakeDataUrl(p.full, NativePhoto.FULL_EDGE).then(function(full){ doCapture({ full: full, small: p.small }); });
});
});
retakeBtn.addEventListener('click', function(){
if (extractAbort) try{ extractAbort.abort(); }catch(e){}
resetToLive();
// camera stream was never stopped — viewfinder is live again with zero re-init latency.
});
saveBtn.addEventListener('click', async function(){
if (saveBtn.disabled) return;
saveBtn.disabled = true; var prevLabel = saveBtn.textContent; saveBtn.textContent = 'Saving…';
var payload = {
vendor: fVendor.value.trim(), mfr: fMfr.value.trim(),
name: fPattern.value.split('—')[0].trim(), color: (fPattern.value.split('—')[1] || '').trim(),
price:'', width:'', repeat:'', substrate:'', how_sold:'', notes:'',
dataUrl: capturedDataUrl, photos:[capturedDataUrl],
require_two:true, front_present:true, back_present:false,
id_source: vendorFromExtract ? 'ocr' : 'manual', commit:true
};
try{
var r = await (await fetch('/api/create-item', {
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)
})).json();
if (r.duplicate){ toast('⚠ ' + (r.err || 'already in catalog')); saveBtn.disabled = false; saveBtn.textContent = prevLabel; return; }
if (!r.ok){ toast('✗ ' + (r.err || 'save failed')); saveBtn.disabled = false; saveBtn.textContent = prevLabel; return; }
sessCount++; sessCountEl.textContent = sessCount;
// dw_unified is a SEPARATE write from the Shopify draft (server.js createNewItem stages
// into new_items_staging) — r.ok only means the Shopify draft succeeded, so it's called
// out here rather than assumed.
var dwuMsg = r.dw_unified
? (r.dw_unified.committed ? ' · ✓ dw_unified' : ' · ⚠ dw_unified failed')
: '';
toast('✅ Saved ' + (r.dw_sku || 'item') + dwuMsg, dwuMsg ? 3600 : undefined);
setTimeout(resetToLive, 550);
}catch(e){ toast('✗ network error'); saveBtn.disabled = false; saveBtn.textContent = prevLabel; }
});
startCamera(facing);
})();
</script>
</body>
</html>