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: — m²
Perimeter: — m
BBox: —
Limit: 9.00 × 8.00 m
OK: Within the 9m × 8m facility limit.
Too large: Exceeds the 9m × 8m facility limit.
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();
}
})();
