/* ============================================================ Stickers, Confetti, Cursor Trail — small reusable building blocks for the Xcela redesign. All exported to window. ============================================================ */ /* Inline SVG stickers — friendlier than emoji and palette-aware (they pull from CSS custom properties). All keep a chunky 2px ink stroke matching the rest of the design. */ const Sticker = ({ shape, size = 60, rot = 0, fill, style = {} }) => { const s = { width: size, height: size, transform: `rotate(${rot}deg)`, ...style }; const ink = "var(--ink)"; const f = fill || "var(--pink)"; switch (shape) { case "star": return ( ); case "heart": return ( ); case "cloud": return ( ); case "blob": return ( ); case "flower": return ( ); case "sun": return ( ); case "rainbow": return ( ); case "spark": return ( ); case "lollipop": return ( ); case "balloon": return ( ); case "moon": return ( ); case "smile": return ( ); case "zigzag": return ( ); default: return null; } }; /* ---------- Confetti burst on enroll click ---------- */ const fireConfetti = (originX, originY) => { const canvas = document.getElementById("confetti"); if (!canvas) return; const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + "px"; canvas.style.height = window.innerHeight + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const colors = [ getCss("--pink"), getCss("--yellow"), getCss("--mint"), getCss("--sky"), getCss("--grape"), getCss("--tangerine"), ]; const particles = []; const N = 140; for (let i = 0; i < N; i++) { const angle = Math.random() * Math.PI * 2; const speed = 6 + Math.random() * 9; particles.push({ x: originX, y: originY, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 6, g: 0.35 + Math.random() * 0.15, size: 6 + Math.random() * 8, rot: Math.random() * Math.PI, vr: (Math.random() - 0.5) * 0.4, color: colors[Math.floor(Math.random() * colors.length)], shape: Math.random() < 0.5 ? "rect" : "circle", life: 1, }); } let lastTime = performance.now(); const step = (now) => { const dt = Math.min(40, now - lastTime); lastTime = now; ctx.clearRect(0, 0, canvas.width, canvas.height); let alive = false; particles.forEach((p) => { p.vy += p.g; p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.life -= 0.006; if (p.life <= 0 || p.y > window.innerHeight + 30) return; alive = true; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.strokeStyle = getCss("--ink"); ctx.lineWidth = 1.5; if (p.shape === "rect") { ctx.fillRect(-p.size / 2, -p.size / 3, p.size, p.size * 0.6); ctx.strokeRect(-p.size / 2, -p.size / 3, p.size, p.size * 0.6); } else { ctx.beginPath(); ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.restore(); }); if (alive) requestAnimationFrame(step); else ctx.clearRect(0, 0, canvas.width, canvas.height); }; requestAnimationFrame(step); }; const getCss = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim() || "#000"; /* ---------- Sparkle cursor trail ---------- */ const initCursorTrail = () => { const colors = ["--pink", "--yellow", "--mint", "--sky", "--grape", "--tangerine"]; let lastTime = 0; let i = 0; const handler = (e) => { const now = performance.now(); if (now - lastTime < 60) return; lastTime = now; const dot = document.createElement("div"); dot.className = "trail-dot"; dot.style.left = (e.clientX - 7) + "px"; dot.style.top = (e.clientY - 7) + "px"; dot.style.background = `var(${colors[i++ % colors.length]})`; document.body.appendChild(dot); setTimeout(() => dot.remove(), 700); }; window.addEventListener("pointermove", handler); return () => window.removeEventListener("pointermove", handler); }; /* ---------- Reveal-on-scroll observer ---------- */ const initReveal = () => { const els = document.querySelectorAll(".reveal"); const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("in"); io.unobserve(entry.target); } }); }, { threshold: 0.12 }); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }; /* ---------- Image placeholder (sticker style) ---------- The original site uses Pixar-style illustrations. We don't have those assets, so we render labelled placeholder tiles with sticker doodles instead — clearly intentional, never trying to fake the real artwork. */ const PlaceholderTile = ({ label, bg, stickers = [] }) => (