Comments on: The Best Artificial Turf for Dogs: 2026 Pet Turf Guide for SoCal Homeowners https://revivelandscape.com/best-artificial-turf-for-dogs/ <div class="rl-widget-root"> <div class="rl-wrap"> <!-- Header --> <div class="rl-header"> <div class="rl-logo">Revive Landscape</div> <h1>See Your Yard<br/>Transformed by AI</h1> <p>Upload a photo, answer 3 quick questions,<br/>and get a free AI design concept in your inbox.</p> </div> <!-- Step indicators --> <div class="rl-steps" id="rl-step-indicators"> <div class="rl-step active" id="ind-1"> <div class="rl-step-dot">1</div> <div class="rl-step-label">Photo</div> </div> <div class="rl-step-line" id="line-1"></div> <div class="rl-step" id="ind-2"> <div class="rl-step-dot">2</div> <div class="rl-step-label">Service</div> </div> <div class="rl-step-line" id="line-2"></div> <div class="rl-step" id="ind-3"> <div class="rl-step-dot">3</div> <div class="rl-step-label">Style</div> </div> <div class="rl-step-line" id="line-3"></div> <div class="rl-step" id="ind-4"> <div class="rl-step-dot">4</div> <div class="rl-step-label">Budget</div> </div> <div class="rl-step-line" id="line-4"></div> <div class="rl-step" id="ind-5"> <div class="rl-step-dot">5</div> <div class="rl-step-label">Get Design</div> </div> </div> <!-- Card --> <div class="rl-card"> <!-- ── Step 1: Photo Upload ── --> <div class="rl-step-panel active" id="panel-1"> <div class="rl-panel-title">Upload a photo of your yard</div> <div class="rl-panel-sub">Any angle works — backyard, front yard, patio area. We'll analyze it to build your concept.</div> <!-- Preview + Drawing canvas overlay --> <div class="rl-preview-wrap" id="rl-preview-wrap"> <img decoding="async" class="rl-preview" id="rl-preview-img" src="" alt="Your yard preview"/> <canvas id="rl-draw-canvas"></canvas> </div> <!-- Drawing controls (shown after photo loads) --> <div class="rl-draw-controls" id="rl-draw-controls"> <div class="rl-draw-hint">🖌 Paint the area you want redesigned</div> <div class="rl-draw-tools"> <button class="rl-tool-btn active" id="btn-tool-paint" onclick="setDrawTool('paint')">🖌 Paint</button> <button class="rl-tool-btn" id="btn-tool-erase" onclick="setDrawTool('erase')">⌫ Erase</button> <button class="rl-tool-btn rl-tool-clear" onclick="clearDrawCanvas()">✕ Clear</button> </div> <div class="rl-brush-row"> <span>Brush:</span> <button class="rl-size-btn active" onclick="setDrawSize(20,this)">S</button> <button class="rl-size-btn" onclick="setDrawSize(40,this)">M</button> <button class="rl-size-btn" onclick="setDrawSize(70,this)">L</button> </div> <div class="rl-draw-optional">Optional — leave blank to redesign the full yard</div> </div> <div class="rl-upload-zone" id="rl-upload-zone"> <div class="rl-upload-icon">📸</div> <p><strong>Drag & drop your photo here</strong><br/>or click to browse<br/><span style="font-size:12px;margin-top:4px;display:block;">JPG, PNG · Max 10MB</span></p> <input type="file" id="rl-file-input" accept="image/jpeg,image/jpg,image/png,image/webp"/> </div> <button class="rl-change-photo" id="rl-change-photo" onclick="resetPhoto()">↩ Use a different photo</button> <div class="rl-error" id="err-1"></div> <button class="rl-btn" id="btn-1" onclick="goToStep(2)" disabled>Continue →</button> </div> <!-- ── Step 2: Service ── --> <div class="rl-step-panel" id="panel-2"> <div class="rl-panel-title">What are you most interested in?</div> <div class="rl-panel-sub">Choose the primary service — we'll design around it.</div> <div class="rl-options"> <div class="rl-option" onclick="selectOption(this,'service','turf')"> <div class="rl-option-icon">🌿</div> <div class="rl-option-label">Artificial Turf</div> <div class="rl-option-sub">Lush, maintenance-free</div> </div> <div class="rl-option" onclick="selectOption(this,'service','pavers')"> <div class="rl-option-icon">🧱</div> <div class="rl-option-label">Pavers</div> <div class="rl-option-sub">Patios & walkways</div> </div> <div class="rl-option" onclick="selectOption(this,'service','concrete')"> <div class="rl-option-icon">⬜</div> <div class="rl-option-label">Concrete</div> <div class="rl-option-sub">Driveways & slabs</div> </div> <div class="rl-option" onclick="selectOption(this,'service','putting-green')"> <div class="rl-option-icon">⛳</div> <div class="rl-option-label">Putting Green</div> <div class="rl-option-sub">Custom greens</div> </div> <div class="rl-option" onclick="selectOption(this,'service','full')"> <div class="rl-option-icon">✨</div> <div class="rl-option-label">Full Landscape</div> <div class="rl-option-sub">Complete transformation</div> </div> </div> <div class="rl-error" id="err-2"></div> <button class="rl-btn" id="btn-2" onclick="goToStep(3)" disabled>Continue →</button> <button class="rl-btn-back" onclick="goToStep(1)">← Back</button> </div> <!-- ── Step 3: Style ── --> <div class="rl-step-panel" id="panel-3"> <div class="rl-panel-title">What's your vibe?</div> <div class="rl-panel-sub">Pick the aesthetic that feels most like you.</div> <div class="rl-options"> <div class="rl-option" onclick="selectOption(this,'style','modern')"> <div class="rl-option-icon">◼</div> <div class="rl-option-label">Modern</div> <div class="rl-option-sub">Clean & minimal</div> </div> <div class="rl-option" onclick="selectOption(this,'style','natural')"> <div class="rl-option-icon">🌱</div> <div class="rl-option-label">Natural</div> <div class="rl-option-sub">Organic & lush</div> </div> <div class="rl-option" onclick="selectOption(this,'style','luxury')"> <div class="rl-option-icon">💎</div> <div class="rl-option-label">Luxury</div> <div class="rl-option-sub">Resort-style finish</div> </div> <div class="rl-option" onclick="selectOption(this,'style','family')"> <div class="rl-option-icon">🏡</div> <div class="rl-option-label">Family</div> <div class="rl-option-sub">Practical & durable</div> </div> </div> <div class="rl-error" id="err-3"></div> <button class="rl-btn" id="btn-3" onclick="goToStep(4)" disabled>Continue →</button> <button class="rl-btn-back" onclick="goToStep(2)">← Back</button> </div> <!-- ── Step 4: Budget ── --> <div class="rl-step-panel" id="panel-4"> <div class="rl-panel-title">What's your rough budget?</div> <div class="rl-panel-sub">This helps us tailor the design to realistic materials and scope.</div> <div class="rl-options" style="grid-template-columns:1fr 1fr;"> <div class="rl-option" onclick="selectOption(this,'budget','Under $5,000')"> <div class="rl-option-icon">💚</div> <div class="rl-option-label">Under $5K</div> </div> <div class="rl-option" onclick="selectOption(this,'budget','$5,000 – $15,000')"> <div class="rl-option-icon">💰</div> <div class="rl-option-label">$5K – $15K</div> </div> <div class="rl-option" onclick="selectOption(this,'budget','$15,000 – $30,000')"> <div class="rl-option-icon">💎</div> <div class="rl-option-label">$15K – $30K</div> </div> <div class="rl-option" onclick="selectOption(this,'budget','$30,000+')"> <div class="rl-option-icon">🏆</div> <div class="rl-option-label">$30K+</div> </div> </div> <div class="rl-error" id="err-4"></div> <button class="rl-btn" id="btn-4" onclick="goToStep(5)" disabled>Continue →</button> <button class="rl-btn-back" onclick="goToStep(3)">← Back</button> </div> <!-- ── Step 5: Contact + Generate ── --> <div class="rl-step-panel" id="panel-5"> <div class="rl-panel-title">Where should we send your design?</div> <div class="rl-panel-sub">Your AI concept will be emailed to you within 30 seconds — completely free.</div> <div class="rl-field"> <label>Your Name</label> <input type="text" id="inp-name" placeholder="John Smith"/> </div> <div class="rl-field"> <label>Email Address <span>*</span></label> <input type="email" id="inp-email" placeholder="john@example.com"/> </div> <div class="rl-field"> <label>Phone Number <span style="color:var(--muted);font-weight:400;">(optional)</span></label> <input type="tel" id="inp-phone" placeholder="(818) 555-0100"/> </div> <div class="rl-error" id="err-5"></div> <button class="rl-btn" id="btn-5" onclick="generateDesign()">🌿 Generate My Free Design</button> <button class="rl-btn-back" onclick="goToStep(4)">← Back</button> </div> <!-- ── Loading ── --> <div id="rl-loading"> <div class="rl-loading-wrap"> <div class="rl-spinner"></div> <div class="rl-loading-title">Creating Your Design</div> <div class="rl-loading-msg" id="rl-loading-msg">Analyzing your yard...</div> <div class="rl-progress-track"> <div class="rl-progress-fill" id="rl-progress-fill"></div> </div> <div class="rl-loading-steps"> <div class="rl-lstep active" id="lstep-1"> <div class="rl-lstep-dot">1</div> <span>Analyzing your yard photo with AI</span> </div> <div class="rl-lstep" id="lstep-2"> <div class="rl-lstep-dot">2</div> <span>Building your design concept</span> </div> <div class="rl-lstep" id="lstep-3"> <div class="rl-lstep-dot">3</div> <span>Rendering high-resolution image</span> </div> <div class="rl-lstep" id="lstep-4"> <div class="rl-lstep-dot">4</div> <span>Sending to your email</span> </div> </div> </div> </div> <!-- ── Success ── --> <div id="rl-success"> <div class="rl-success-wrap"> <div class="rl-success-icon">✓</div> <div class="rl-success-title">Your Design Is On Its Way!</div> <div class="rl-success-sub">We've sent your AI-generated yard concept to your inbox. Check your email — it should arrive in the next minute.</div> <div class="rl-success-email" id="success-email-display"></div> <div class="rl-success-sub" style="font-size:13px;margin-bottom:20px;">Love what you see? Our team is ready to bring it to life with a free on-site consultation.</div> <a href="/contact-us/" class="rl-success-cta">Book a Free Consultation →</a> </div> </div> </div><!-- /.rl-card --> <p style="text-align:center;color:rgba(255,255,255,0.2);font-size:12px;margin-top:20px;"> Powered by AI · Your info is never shared · Revive Landscape · (818) 257-1540 </p> </div><!-- /.rl-wrap --> <style> /* ── Drawing overlay styles ── */ .rl-preview-wrap { position: relative; display: none; margin: 16px auto; border-radius: 12px; overflow: hidden; line-height: 0; box-shadow: 0 4px 24px rgba(0,0,0,0.4); } .rl-preview-wrap.visible { display: block; } .rl-preview-wrap .rl-preview { display: block; width: 100%; height: auto; border-radius: 0; margin: 0; } #rl-draw-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; touch-action: none; border-radius: 0; } /* Drawing controls panel */ .rl-draw-controls { display: none; background: rgba(106,176,76,0.06); border: 1px solid rgba(106,176,76,0.2); border-radius: 10px; padding: 12px 14px; margin: 10px 0 6px; } .rl-draw-controls.visible { display: block; } .rl-draw-hint { font-size: 13px; font-weight: 600; color: #6ab04c; text-align: center; margin-bottom: 10px; } .rl-draw-tools { display: flex; gap: 8px; justify-content: center; margin-bottom: 10px; flex-wrap: wrap; } .rl-tool-btn { background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.15); color: rgba(255,255,255,0.65); border-radius: 6px; padding: 6px 14px; font-size: 13px; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; line-height: 1.4; } .rl-tool-btn:hover { background: rgba(255,255,255,0.12); } .rl-tool-btn.active { background: rgba(106,176,76,0.18); border-color: #6ab04c; color: #6ab04c; } .rl-tool-clear:hover { background: rgba(200,50,50,0.15) !important; border-color: rgba(200,80,80,0.5) !important; color: #f99 !important; } .rl-brush-row { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 12px; color: rgba(255,255,255,0.45); } .rl-size-btn { background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.15); color: rgba(255,255,255,0.65); border-radius: 5px; width: 30px; height: 30px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; } .rl-size-btn:hover { background: rgba(255,255,255,0.12); } .rl-size-btn.active { background: rgba(106,176,76,0.18); border-color: #6ab04c; color: #6ab04c; } .rl-draw-optional { text-align: center; font-size: 11px; color: rgba(255,255,255,0.28); margin-top: 8px; } </style> <script> // ── State ── var state = { currentStep: 1, photoBase64: null, // JPEG, original aspect ratio (up to 1024px) maskBase64: null, // PNG, square-cropped, transparent=edit / opaque=keep squarePhotoBase64: null, // PNG, square-cropped version of photo (sent when mask is used) service: null, style: null, budget: null, }; var ENDPOINT = 'https://klywmcypitibkelisfyx.supabase.co/functions/v1/generate-yard-design'; // ── Drawing state ── var drawState = { active: false, tool: 'paint', // 'paint' | 'erase' brushSize: 20, canvas: null, ctx: null, naturalW: 0, naturalH: 0, lastX: 0, lastY: 0, initialized: false, }; // ── Photo handling ── var fileInput = document.getElementById('rl-file-input'); var uploadZone = document.getElementById('rl-upload-zone'); var previewImg = document.getElementById('rl-preview-img'); var changeBtn = document.getElementById('rl-change-photo'); fileInput.addEventListener('change', function(e) { var file = e.target.files[0]; if (file) handleFile(file); }); uploadZone.addEventListener('click', function(e) { if (e.target !== fileInput) fileInput.click(); }); uploadZone.addEventListener('dragover', function(e) { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', function() { uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); var file = e.dataTransfer.files[0]; if (file) handleFile(file); }); function handleFile(file) { if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { showError('err-1', 'Please upload a JPG or PNG image.'); return; } if (file.size > 10 * 1024 * 1024) { showError('err-1', 'Image is too large. Please use an image under 10MB.'); return; } hideError('err-1'); // Resize and compress to max 1024px on longest side var reader = new FileReader(); reader.onload = function(e) { var img = new Image(); img.onload = function() { var canvas = document.createElement('canvas'); var MAX = 1024; var w = img.width, h = img.height; if (w > MAX || h > MAX) { if (w > h) { h = Math.round(h * MAX / w); w = MAX; } else { w = Math.round(w * MAX / h); h = MAX; } } canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); var b64 = canvas.toDataURL('image/jpeg', 0.85).split(',')[1]; state.photoBase64 = b64; // Show preview previewImg.src = 'data:image/jpeg;base64,' + b64; previewImg.onload = function() { // Now init the drawing canvas with the photo's natural dimensions initDrawingCanvas(w, h); }; document.getElementById('rl-preview-wrap').classList.add('visible'); uploadZone.style.display = 'none'; changeBtn.classList.add('visible'); document.getElementById('btn-1').disabled = false; }; img.src = e.target.result; }; reader.readAsDataURL(file); } function resetPhoto() { state.photoBase64 = null; state.maskBase64 = null; state.squarePhotoBase64 = null; // Reset drawing state if (drawState.canvas) { drawState.ctx.clearRect(0, 0, drawState.naturalW, drawState.naturalH); drawState.initialized = false; } document.getElementById('rl-preview-wrap').classList.remove('visible'); previewImg.src = ''; uploadZone.style.display = ''; changeBtn.classList.remove('visible'); document.getElementById('rl-draw-controls').classList.remove('visible'); document.getElementById('btn-1').disabled = true; fileInput.value = ''; // Reset tool buttons setDrawTool('paint'); setDrawSize(20, document.querySelector('.rl-size-btn')); } // ── Drawing canvas ── function initDrawingCanvas(naturalW, naturalH) { var canvas = document.getElementById('rl-draw-canvas'); drawState.canvas = canvas; drawState.ctx = canvas.getContext('2d'); drawState.naturalW = naturalW; drawState.naturalH = naturalH; // Set canvas pixel dimensions to match photo canvas.width = naturalW; canvas.height = naturalH; // Show drawing controls document.getElementById('rl-draw-controls').classList.add('visible'); if (drawState.initialized) return; // don't re-attach events drawState.initialized = true; // ── Mouse events ── canvas.addEventListener('mousedown', function(e) { e.preventDefault(); drawState.active = true; var pos = getCanvasPos(canvas, e.clientX, e.clientY); drawState.lastX = pos.x; drawState.lastY = pos.y; paintAt(pos.x, pos.y); }); canvas.addEventListener('mousemove', function(e) { if (!drawState.active) return; var pos = getCanvasPos(canvas, e.clientX, e.clientY); paintLine(drawState.lastX, drawState.lastY, pos.x, pos.y); drawState.lastX = pos.x; drawState.lastY = pos.y; }); canvas.addEventListener('mouseup', function() { drawState.active = false; }); canvas.addEventListener('mouseleave', function() { drawState.active = false; }); // ── Touch events ── canvas.addEventListener('touchstart', function(e) { e.preventDefault(); drawState.active = true; var t = e.touches[0]; var pos = getCanvasPos(canvas, t.clientX, t.clientY); drawState.lastX = pos.x; drawState.lastY = pos.y; paintAt(pos.x, pos.y); }, { passive: false }); canvas.addEventListener('touchmove', function(e) { e.preventDefault(); if (!drawState.active) return; var t = e.touches[0]; var pos = getCanvasPos(canvas, t.clientX, t.clientY); paintLine(drawState.lastX, drawState.lastY, pos.x, pos.y); drawState.lastX = pos.x; drawState.lastY = pos.y; }, { passive: false }); canvas.addEventListener('touchend', function() { drawState.active = false; }); canvas.addEventListener('touchcancel',function() { drawState.active = false; }); } function getCanvasPos(canvas, clientX, clientY) { var rect = canvas.getBoundingClientRect(); var scaleX = canvas.width / rect.width; var scaleY = canvas.height / rect.height; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY, }; } function paintAt(x, y) { var ctx = drawState.ctx; var half = drawState.brushSize / 2; if (drawState.tool === 'erase') { ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = 'rgba(0,0,0,1)'; } else { ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = 'rgba(106,176,76,0.55)'; } ctx.beginPath(); ctx.arc(x, y, half, 0, Math.PI * 2); ctx.fill(); } function paintLine(x1, y1, x2, y2) { var dist = Math.hypot(x2 - x1, y2 - y1); var steps = Math.max(1, Math.floor(dist / (drawState.brushSize * 0.25))); for (var i = 0; i <= steps; i++) { var t = i / steps; paintAt(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t); } } function setDrawTool(tool) { drawState.tool = tool; document.getElementById('btn-tool-paint').classList.toggle('active', tool === 'paint'); document.getElementById('btn-tool-erase').classList.toggle('active', tool === 'erase'); if (drawState.ctx) { drawState.ctx.globalCompositeOperation = 'source-over'; } } function setDrawSize(size, btn) { drawState.brushSize = size; document.querySelectorAll('.rl-size-btn').forEach(function(b) { b.classList.remove('active'); }); if (btn) btn.classList.add('active'); } function clearDrawCanvas() { if (drawState.ctx) { drawState.ctx.clearRect(0, 0, drawState.naturalW, drawState.naturalH); } } function hasPainted() { if (!drawState.ctx) return false; var data = drawState.ctx.getImageData(0, 0, drawState.naturalW, drawState.naturalH).data; for (var i = 3; i < data.length; i += 4) { if (data[i] > 10) return true; } return false; } // Export a square-cropped PNG mask: // transparent (alpha=0) where user painted → OpenAI edits here // opaque white (alpha=255) where not painted → OpenAI keeps this area function exportMask() { var w = drawState.naturalW; var h = drawState.naturalH; var size = Math.min(w, h); var offX = Math.floor((w - size) / 2); var offY = Math.floor((h - size) / 2); var srcData = drawState.ctx.getImageData(0, 0, w, h); var maskCanvas = document.createElement('canvas'); maskCanvas.width = size; maskCanvas.height = size; var maskCtx = maskCanvas.getContext('2d'); var maskData = maskCtx.createImageData(size, size); for (var y = 0; y < size; y++) { for (var x = 0; x < size; x++) { var si = ((y + offY) * w + (x + offX)) * 4; var di = (y * size + x) * 4; var alpha = srcData.data[si + 3]; if (alpha > 10) { // Painted: transparent — OpenAI edits this area maskData.data[di] = 0; maskData.data[di+1] = 0; maskData.data[di+2] = 0; maskData.data[di+3] = 0; } else { // Unpainted: opaque white — OpenAI keeps this area maskData.data[di] = 255; maskData.data[di+1] = 255; maskData.data[di+2] = 255; maskData.data[di+3] = 255; } } } maskCtx.putImageData(maskData, 0, 0); return maskCanvas.toDataURL('image/png').split(',')[1]; } // Export a square-cropped PNG version of the photo (same crop as mask) function exportSquarePhoto() { var w = drawState.naturalW; var h = drawState.naturalH; var size = Math.min(w, h); var offX = Math.floor((w - size) / 2); var offY = Math.floor((h - size) / 2); var sqCanvas = document.createElement('canvas'); sqCanvas.width = size; sqCanvas.height = size; var sqCtx = sqCanvas.getContext('2d'); // Draw from the preview image using same center-crop offsets sqCtx.drawImage(previewImg, offX, offY, size, size, 0, 0, size, size); return sqCanvas.toDataURL('image/png').split(',')[1]; } // ── Option selection ── function selectOption(el, key, value) { var siblings = el.parentElement.querySelectorAll('.rl-option'); siblings.forEach(function(s) { s.classList.remove('selected'); }); el.classList.add('selected'); state[key] = value; var btnMap = { service: 'btn-2', style: 'btn-3', budget: 'btn-4' }; document.getElementById(btnMap[key]).disabled = false; hideError('err-' + { service: 2, style: 3, budget: 4 }[key]); } // ── Step navigation ── function goToStep(n) { // Validate current step before advancing if (n > state.currentStep) { if (state.currentStep === 1) { if (!state.photoBase64) { showError('err-1', 'Please upload a photo of your yard first.'); return; } // Export mask & square photo when leaving Step 1 if (hasPainted()) { state.maskBase64 = exportMask(); state.squarePhotoBase64 = exportSquarePhoto(); } else { state.maskBase64 = null; state.squarePhotoBase64 = null; } } if (state.currentStep === 2 && !state.service) { showError('err-2', 'Please select a service.'); return; } if (state.currentStep === 3 && !state.style) { showError('err-3', 'Please select a style.'); return; } if (state.currentStep === 4 && !state.budget) { showError('err-4', 'Please select a budget range.'); return; } } document.getElementById('panel-' + state.currentStep).classList.remove('active'); document.getElementById('panel-' + n).classList.add('active'); // Update step indicators for (var i = 1; i <= 5; i++) { var ind = document.getElementById('ind-' + i); ind.classList.remove('active', 'done'); if (i < n) ind.classList.add('done'); else if (i === n) ind.classList.add('active'); if (i < 5) { document.getElementById('line-' + i).classList.toggle('done', i < n); } ind.querySelector('.rl-step-dot').textContent = i < n ? '✓' : i; } state.currentStep = n; window.scrollTo({ top: 0, behavior: 'smooth' }); } // ── Generate ── function generateDesign() { var email = document.getElementById('inp-email').value.trim(); var name = document.getElementById('inp-name').value.trim(); var phone = document.getElementById('inp-phone').value.trim(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { showError('err-5', 'Please enter a valid email address.'); return; } hideError('err-5'); // Hide form, show loading document.getElementById('rl-step-indicators').style.display = 'none'; document.getElementById('panel-5').classList.remove('active'); document.getElementById('rl-loading').style.display = 'block'; // Animated loading steps var steps = [ { id: 'lstep-1', msg: 'Analyzing your yard with AI...', prog: 20 }, { id: 'lstep-2', msg: 'Designing your custom concept...', prog: 50 }, { id: 'lstep-3', msg: 'Rendering your high-res design...', prog: 80 }, { id: 'lstep-4', msg: 'Sending to your inbox...', prog: 95 }, ]; var delays = [0, 5000, 12000, 20000]; steps.forEach(function(s, i) { setTimeout(function() { if (i > 0) { var prev = document.getElementById(steps[i-1].id); prev.classList.remove('active'); prev.classList.add('done'); prev.querySelector('.rl-lstep-dot').textContent = '✓'; } document.getElementById(s.id).classList.add('active'); document.getElementById('rl-loading-msg').textContent = s.msg; document.getElementById('rl-progress-fill').style.width = s.prog + '%'; }, delays[i]); }); // Use square photo + mask if user painted, else send original photo var photoToSend = state.squarePhotoBase64 || state.photoBase64; // API call fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, name: name, phone: phone, service: state.service, style: state.style, budget: state.budget, photoBase64: photoToSend, maskBase64: state.maskBase64 || null, }), }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.error) throw new Error(data.error); // Final loading step done var lastStep = document.getElementById('lstep-4'); lastStep.classList.remove('active'); lastStep.classList.add('done'); lastStep.querySelector('.rl-lstep-dot').textContent = '✓'; document.getElementById('rl-progress-fill').style.width = '100%'; setTimeout(function() { document.getElementById('rl-loading').style.display = 'none'; document.getElementById('rl-success').style.display = 'block'; document.getElementById('success-email-display').textContent = '✉ Design sent to: ' + email; }, 800); }) .catch(function(err) { document.getElementById('rl-loading').style.display = 'none'; document.getElementById('rl-step-indicators').style.display = ''; document.getElementById('panel-5').classList.add('active'); showError('err-5', 'Something went wrong — please try again. (' + err.message + ')'); }); } // ── Helpers ── function showError(id, msg) { var el = document.getElementById(id); if (el) { el.textContent = msg; el.classList.add('visible'); } } function hideError(id) { var el = document.getElementById(id); if (el) { el.textContent = ''; el.classList.remove('visible'); } } </script> </div> Sun, 24 May 2026 19:56:34 +0000 hourly 1 https://wordpress.org/?v=6.9.4