shadesails2you

PART A — WPBakery Raw HTML (paste into a Raw HTML element)

DIY Shade Sail Order — Visualise → Confirm → Pay

Manufacturing limit: 9m × 8m. Drag points or enter AB/BC/CD/DA/AC.

Step 1 — Visualiser

Checkout uses 4 corners (A–B–C–D) + diagonal AC.
Drag points • Double-click guides
Points Edges Area: Perimeter: m BBox: Limit: 9.00 × 8.00 m

Step 2 — Measurements (metres)

Auto-fills from canvas when 4 corners. You can type to rebuild the shape too.

Step 3 — Order & Pay

(function(){ function run(){ const root = document.getElementById('cwDIY'); if(!root) return; const canvas = root.querySelector('#cw_sailCanvas'); if(!canvas) return; const ctx = canvas.getContext('2d'); const pointsInput = root.querySelector('#cw_points'); const gridMetersInput = root.querySelector('#cw_gridMeters'); const resetBtn = root.querySelector('#cw_resetShape'); const areaOut = root.querySelector('#cw_areaOut'); const perimOut = root.querySelector('#cw_perimOut'); const bboxOut = root.querySelector('#cw_bboxOut'); const limitOk = root.querySelector('#cw_limitOk'); const limitBad = root.querySelector('#cw_limitBad'); const m_ab = root.querySelector('#m_ab'); const m_bc = root.querySelector('#m_bc'); const m_cd = root.querySelector('#m_cd'); const m_da = root.querySelector('#m_da'); const m_ac = root.querySelector('#m_ac'); const confirmBtn = root.querySelector('#cw_confirmBtn'); const confirmStatus = root.querySelector('#cw_confirmStatus'); const orderBtn = root.querySelector('#cw_orderPayBtn'); const copyBtn = root.querySelector('#cw_copyBtn'); const statusOut = root.querySelector('#cw_status'); const rigSel = root.querySelector('#cw_rigging'); const rigSize = root.querySelector('#cw_rigsize'); rigSel.addEventListener('change', ()=>{ rigSize.disabled = rigSel.value !== 'yes'; }); const accessSel = root.querySelector('#cw_access'); const FAC_W = 9, FAC_H = 8; let pxPerMeter = 40; let gridMeters = 1; let dragging = null; let helperGuides = []; let pts = []; let locked = false; function setGridMeters(m){ gridMeters = m; pxPerMeter = 40 / gridMeters; } function toMeters(px){ return px / pxPerMeter; } function dist(a,b){ return Math.hypot(a.x-b.x, a.y-b.y); } function round2(x){ return Math.round(x*100)/100; } function heron(a,b,c){ const s=(a+b+c)/2; const v=s*(s-a)*(s-b)*(s-c); if(v<=0) return 0; return Math.sqrt(v); } function areaMeters2(){ if(pts.length<3) return 0; let sum=0; for(let i=0;imaxX)maxX=p.x; if(p.ymaxY)maxY=p.y; } return { w: toMeters(maxX-minX), h: toMeters(maxY-minY) }; } function isWithinFacility(){ const bb = bboxMeters(); return (bb.w <= FAC_W && bb.h <= FAC_H); } function currentABCDAC(){ if(pts.length !== 4) return null; const A=pts[0], B=pts[1], C=pts[2], D=pts[3]; const ab = dist(A,B)/pxPerMeter; const bc = dist(B,C)/pxPerMeter; const cd = dist(C,D)/pxPerMeter; const da = dist(D,A)/pxPerMeter; const ac = dist(A,C)/pxPerMeter; return {ab,bc,cd,da,ac}; } function syncInputsFromCanvas(){ if(locked) return; if(pts.length !== 4) return; const d = currentABCDAC(); if(!d) return; m_ab.value = d.ab.toFixed(2); m_bc.value = d.bc.toFixed(2); m_cd.value = d.cd.toFixed(2); m_da.value = d.da.toFixed(2); m_ac.value = d.ac.toFixed(2); } function syncCanvasFromInputs(){ if(locked) return; if(pts.length !== 4) return; const ab = parseFloat(m_ab.value||'0'); const bc = parseFloat(m_bc.value||'0'); const cd = parseFloat(m_cd.value||'0'); const da = parseFloat(m_da.value||'0'); const ac = parseFloat(m_ac.value||'0'); if(!(ab>0 && bc>0 && cd>0 && da>0 && ac>0)) return; const cosB = (ab*ab + ac*ac - bc*bc) / (2*ab*ac); const cosD = (da*da + ac*ac - cd*cd) / (2*da*ac); if(!isFinite(cosB) || !isFinite(cosD)) return; const cB = Math.max(-1, Math.min(1, cosB)); const cD = Math.max(-1, Math.min(1, cosD)); const xB = ab * cB; const yB = Math.sqrt(Math.max(0, ab*ab - xB*xB)); const xD = da * cD; const yD = -Math.sqrt(Math.max(0, da*da - xD*xD)); const ptsM = [ {x:0, y:0}, {x:xB, y:yB}, {x:ac, y:0}, {x:xD, y:yD}, ]; let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity; for(const p of ptsM){ minX=Math.min(minX,p.x); maxX=Math.max(maxX,p.x); minY=Math.min(minY,p.y); maxY=Math.max(maxY,p.y); } const spanX = Math.max(0.001, maxX-minX); const spanY = Math.max(0.001, maxY-minY); const pad = 40; const sx = (canvas.width - pad*2) / spanX; const sy = (canvas.height - pad*2) / spanY; const s = Math.min(sx, sy); pts = ptsM.map(p => ({ x: pad + (p.x - minX) * s, y: pad + (maxY - p.y) * s, })); render(); recalc(); } function updateLimitBanners(){ const ok = isWithinFacility(); limitOk.classList.toggle('show', ok); limitBad.classList.toggle('show', !ok); } function drawGrid(){ const g = ctx.createLinearGradient(0,0,0,canvas.height); g.addColorStop(0,'#ffffff'); g.addColorStop(1,'#f7fbff'); ctx.fillStyle = g; ctx.fillRect(0,0,canvas.width,canvas.height); const step=40; ctx.strokeStyle='rgba(11,9,191,.12)'; ctx.lineWidth=1; for(let x=0;x{ ctx.beginPath(); if(gd.type==='v'){ ctx.moveTo(gd.x,0); ctx.lineTo(gd.x,canvas.height); } else { ctx.moveTo(0,gd.y); ctx.lineTo(canvas.width,gd.y); } ctx.stroke(); }); ctx.setLineDash([]); } function drawSail(){ if(pts.length===0) return; ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); for(let i=1;i=0;i--){ const p=pts[i]; if(Math.hypot(x-p.x,y-p.y) <= 12+3) return i; } return -1; } function initDefaultShape(n=4){ pts = []; const cx = canvas.width/2, cy = canvas.height/2; const r = Math.min(canvas.width, canvas.height) * 0.30; for(let i=0;i i.disabled = false); } function updateMetrics(){ const A = areaMeters2(), P = perimeterMeters(), BB = bboxMeters(); areaOut.textContent = (A>0)? round2(A).toFixed(2) : '—'; perimOut.textContent = (P>0)? round2(P).toFixed(2) : '—'; bboxOut.textContent = `${round2(BB.w).toFixed(2)} × ${round2(BB.h).toFixed(2)} m`; updateLimitBanners(); } function recalc(){ updateMetrics(); if(locked){ confirmStatus.textContent = 'Confirmed ✅ You can Order & Pay.'; } else { confirmStatus.textContent = (pts.length===4) ? 'When it looks right, click Confirm.' : 'Set corners to 4 for checkout.'; } } canvas.addEventListener('mousedown', (e)=>{ if(locked) return; const r = canvas.getBoundingClientRect(); const x = (e.clientX - r.left) * (canvas.width/r.width); const y = (e.clientY - r.top) * (canvas.height/r.height); const i = hitTest(x,y); if(i>=0) dragging={i,dx:x-pts[i].x,dy:y-pts[i].y}; }); window.addEventListener('mousemove', (e)=>{ if(!dragging || locked) return; const r = canvas.getBoundingClientRect(); const x = (e.clientX - r.left) * (canvas.width/r.width); const y = (e.clientY - r.top) * (canvas.height/r.height); pts[dragging.i].x = Math.max(0, Math.min(canvas.width, x - dragging.dx)); pts[dragging.i].y = Math.max(0, Math.min(canvas.height, y - dragging.dy)); render(); recalc(); }); window.addEventListener('mouseup', ()=> dragging=null); canvas.addEventListener('dblclick', (e)=>{ const r = canvas.getBoundingClientRect(); const x = (e.clientX - r.left) * (canvas.width/r.width); const y = (e.clientY - r.top) * (canvas.height/r.height); const type = (helperGuides.length%2===0)?'v':'h'; helperGuides.push(type==='v'?{type:'v',x}:{type:'h',y}); render(); }); pointsInput.addEventListener('change', ()=>{ const n = Math.max(3, Math.min(6, parseInt(pointsInput.value,10)||4)); pointsInput.value = String(n); initDefaultShape(n); render(); recalc(); }); gridMetersInput.addEventListener('change', ()=>{ const m = Math.max(0.25, Math.min(5, parseFloat(gridMetersInput.value)||1)); gridMetersInput.value = String(m); setGridMeters(m); render(); recalc(); }); resetBtn.addEventListener('click', ()=>{ initDefaultShape(parseInt(pointsInput.value,10)||4); render(); recalc(); }); [m_ab,m_bc,m_cd,m_da,m_ac].forEach(inp=>{ inp.addEventListener('input', ()=>{ syncCanvasFromInputs(); }); }); confirmBtn.addEventListener('click', ()=>{ if(pts.length !== 4){ alert('Set corners to 4 for ordering (A–B–C–D).'); return; } if(!isWithinFacility()){ alert('Too large for manufacturing. Must fit within 9m × 8m.'); return; } const d = currentABCDAC(); const area = heron(d.ab,d.bc,d.ac) + heron(d.da,d.cd,d.ac); if(!(area>0)){ alert('Invalid measurements. Please check AB/BC/CD/DA/AC.'); return; } locked = true; [m_ab,m_bc,m_cd,m_da,m_ac].forEach(i=> i.disabled = true); confirmBtn.disabled = true; confirmBtn.textContent = 'Confirmed ✅'; confirmStatus.textContent = 'Confirmed ✅ You can Order & Pay.'; }); function makeSummary(){ const d = currentABCDAC(); const area = d ? (heron(d.ab,d.bc,d.ac) + heron(d.da,d.cd,d.ac)) : 0; const perim = d ? (d.ab+d.bc+d.cd+d.da) : 0; const bb = bboxMeters(); return [ 'DIY Shade Sail Order (Quad A–B–C–D)', `Customer: ${(root.querySelector('#cw_name').value||'').trim() || '(no name)'} • ${(root.querySelector('#cw_email').value||'').trim() || '(no email)'}`, `Phone: ${(root.querySelector('#cw_phone').value||'').trim() || '(n/a)'} • Suburb: ${(root.querySelector('#cw_suburb').value||'').trim() || '(n/a)'}`, d ? `Measurements (m): AB ${d.ab.toFixed(2)}, BC ${d.bc.toFixed(2)}, CD ${d.cd.toFixed(2)}, DA ${d.da.toFixed(2)}, AC ${d.ac.toFixed(2)}` : 'Measurements: (not set)', `Area: ${round2(area).toFixed(2)} m² • Perimeter: ${round2(perim).toFixed(2)} m`, `BBox: ${round2(bb.w).toFixed(2)}×${round2(bb.h).toFixed(2)} m • Limit 9×8`, `Material: ${root.querySelector('#cw_material').value} • Colour: ${root.querySelector('#cw_colour').value}`, `Rigging: ${root.querySelector('#cw_rigging').value}${root.querySelector('#cw_rigging').value==='yes' ? (' (' + root.querySelector('#cw_rigsize').value + ')') : ''}`, `Shipping: ${root.querySelector('#cw_ship').value}`, `Access: ${root.querySelector('#cw_access').value}`, `Notes: ${(root.querySelector('#cw_notes').value||'').trim() || '(none)'}` ].join('\n'); } copyBtn.addEventListener('click', ()=>{ navigator.clipboard.writeText(makeSummary()).then(()=>{ const old = copyBtn.textContent; copyBtn.textContent='Copied!'; setTimeout(()=>copyBtn.textContent=old,1200); }); }); orderBtn.addEventListener('click', async ()=>{ statusOut.textContent = ''; const email = (root.querySelector('#cw_email').value||'').trim(); if(!email){ alert('Please enter an email.'); return; } if(!locked){ alert('Please Confirm measurements first.'); return; } if(pts.length !== 4){ alert('Corners must be 4 for checkout.'); return; } if(!isWithinFacility()){ alert('Too large for 9m × 8m facility limit.'); return; } const photo = root.querySelector('#cw_photo').files && root.querySelector('#cw_photo').files[0]; if(!photo){ alert('Please choose a sketch/photo file (required).'); return; } const d = currentABCDAC(); const area = heron(d.ab,d.bc,d.ac) + heron(d.da,d.cd,d.ac); if(!(area>0)){ alert('Invalid measurements.'); return; } const payload = { shape: 'quad', quad: { ab:d.ab, bc:d.bc, cd:d.cd, da:d.da, ac:d.ac }, customer: { name: (root.querySelector('#cw_name').value||'').trim(), phone:(root.querySelector('#cw_phone').value||'').trim(), email, suburb:(root.querySelector('#cw_suburb').value||'').trim(), }, options: { access: parseFloat(accessSel.value || '1.0') }, meta: { material: root.querySelector('#cw_material').value, colour: root.querySelector('#cw_colour').value, rigging: root.querySelector('#cw_rigging').value, rigsize: root.querySelector('#cw_rigsize').value, shipping: root.querySelector('#cw_ship').value, }, notes: (root.querySelector('#cw_notes').value||'').trim(), }; orderBtn.disabled = true; const old = orderBtn.textContent; orderBtn.textContent = 'Opening checkout…'; try{ const res = await fetch('/wp-json/cw/v1/create-checkout', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload), }); const data = await res.json(); if(!res.ok) throw new Error(data.error || 'Checkout failed'); window.location.href = data.url; }catch(err){ alert(err.message || String(err)); orderBtn.disabled = false; orderBtn.textContent = old; statusOut.textContent = 'Could not open checkout. Check plugin Stripe settings.'; } }); setGridMeters(1); initDefaultShape(parseInt(pointsInput.value,10) || 4); render(); recalc(); } if(document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', run); } else { run(); } })();