// Act 1 (Problem) + Act 2 (Solution intro) — 0-30s // Time map: // 0-5s Shot 1: WhatsApp chaos // 5-10s Shot 2: Fake visit at cafe // 10-15s Shot 3: Paper receipts tampering // 15-20s Shot 4: 4-quadrant montage "is this your company?" // 20-30s Shot 5: Solution reveal — logo builds // ══════════════════════════════════════════════════════════ // SHOT 1: WhatsApp Chaos // ══════════════════════════════════════════════════════════ function Shot1_Chaos() { const { localTime } = useSprite(); // 20+ whatsapp bubbles that pile up rapidly const bubbles = React.useMemo(() => { const texts = [ 'وين الطلب؟', 'أحمد راسل', 'صار التحصيل؟', 'د. سارة وين', 'الخصم كم؟', 'اتصل بيّ', 'ممكن تحديث؟', 'التقرير لهسة ما وصل', 'مشكلة بالصيدلية', 'أكد الزيارة', 'راح أشوف', 'هسة أرجع', 'محتاج توقيع', 'الكمية قليلة', 'ما أقدر اليوم', 'بكرة يمعود', 'صار؟ شبيك ساكت', 'الطلب ضايع', 'الإيصال وين', 'جواب لو سمحت', ]; return texts.map((text, i) => ({ text, // deterministic positions (no random to avoid re-renders) x: 80 + ((i * 127) % 1100), y: 130 + ((i * 83) % 520), appearAt: 0.15 + i * 0.2, isOwn: i % 2 === 0, })); }, []); // Counter increases const counterN = Math.min(Math.floor(localTime * 6) + 2, 89); // Exit fade last 0.5s of shot (shot is 5s total) const exitT = localTime > 4.5 ? (localTime - 4.5) / 0.5 : 0; const exitFade = 1 - exitT; // End-of-shot question appears const qT = clamp((localTime - 3.5) / 0.7, 0, 1); // Tick sound on each new bubble const lastCount = React.useRef(0); React.useEffect(() => { const shown = bubbles.filter(b => localTime >= b.appearAt).length; if (shown > lastCount.current) { if (window.FajrAudio) window.FajrAudio.play('soft-tick', { freq: 1400 + Math.random() * 600, gain: 0.05 }); } lastCount.current = shown; }, [localTime, bubbles]); return (
{/* Faux phone messages background */} {bubbles.map((b, i) => { if (localTime < b.appearAt) return null; const t = clamp((localTime - b.appearAt) / 0.35, 0, 1); const jitter = Math.sin((localTime + i) * 3) * 2; return (
{b.text}
); })} {/* Darkening overlay */}
{/* Counter top-center */}
{counterN} رسالة غير مقروءة
{/* Question */}
شركتك تدير عملياتها بالواتساب؟
); } // ══════════════════════════════════════════════════════════ // SHOT 2: Fake visit from cafe // ══════════════════════════════════════════════════════════ function Shot2_FakeVisit() { const { localTime } = useSprite(); // Types "زرت د. أحمد — الكرادة ✓" progressively const typedText = 'زرت د. أحمد — الكرادة ✓'; const chars = Math.floor(clamp((localTime - 1.0) / 1.5, 0, 1) * typedText.length); const shown = typedText.slice(0, chars); // Dashed red line draws between cafe and clinic after 2.8s const lineT = clamp((localTime - 2.8) / 1.0, 0, 1); // Alert appears at 3.8s const alertT = clamp((localTime - 3.8) / 0.5, 0, 1); // Exit const exitT = localTime > 4.5 ? (localTime - 4.5) / 0.5 : 0; const exitFade = 1 - exitT; // Typing sounds const lastChars = React.useRef(0); React.useEffect(() => { if (chars > lastChars.current) { if (window.FajrAudio) window.FajrAudio.play('soft-tick', { freq: 1600 + Math.random() * 400, gain: 0.07 }); } lastChars.current = chars; }, [chars]); return (
{/* Left: Phone mockup (at cafe) */}
NEW VISIT LOG
{shown} {chars < typedText.length && localTime > 1.0 && (   )}
{chars >= typedText.length && (
حفظ الزيارة
)}
{/* "at cafe" label */}
☕ المندوب في مقهى
{/* Right: Map */}
LIVE LOCATION MAP
{/* Cafe pin (where rep actually is) — bottom-left */}
CAFE
{/* Clinic pin (where visit was claimed) — top-right */}
CLINIC
{/* Dashed red line between */} {lineT > 0.9 && ( 5.2 km )}
{/* Alert at bottom */}
مندوبك يسجّل زيارات ما صارت؟
); } // ══════════════════════════════════════════════════════════ // SHOT 3: Paper receipts tampering // ══════════════════════════════════════════════════════════ function Shot3_PaperReceipts() { const { localTime } = useSprite(); // Two paper receipts appear, both with same number (duplicate) const r1T = clamp(localTime / 0.6, 0, 1); const r2T = clamp((localTime - 1.0) / 0.6, 0, 1); // Red stamp appears on top showing "duplicate" const stampT = clamp((localTime - 2.0) / 0.5, 0, 1); // Excel shakes const shake = Math.sin(localTime * 30) * (localTime > 2.8 ? 3 : 0); // Alert const alertT = clamp((localTime - 3.5) / 0.5, 0, 1); // Exit const exitT = localTime > 4.5 ? (localTime - 4.5) / 0.5 : 0; const exitFade = 1 - exitT; // Stamp sound const stampFired = React.useRef(false); React.useEffect(() => { if (localTime > 2.0 && localTime < 2.1 && !stampFired.current) { stampFired.current = true; if (window.FajrAudio) window.FajrAudio.play('pop', { freq: 180, gain: 0.3 }); } if (localTime < 1.9) stampFired.current = false; }, [localTime]); return (
{/* Receipt 1 — paper style */}
إيصال تحصيل
رقم: #RC-0847
الصيدلية: النور
المبلغ: 500,000
التاريخ: 15/04
ختم الصيدلية: _______
{/* Receipt 2 — SAME number (duplicate/tampered) */}
إيصال تحصيل
رقم: #RC-0847
الصيدلية: النور
المبلغ: 500,000 350,000
التاريخ: 15/04
ختم الصيدلية: _______
{/* RED STAMP overlay */} {stampT > 0 && (
مكرّر!
)}
{/* Excel card (right) */}
collections_april.xlsx
{[ ['الصيدلية', 'الرقم', 'المبلغ'], ['النور', '0847', '500,000'], ['النور', '0847', '350,000'], ['الحياة', '0848', '800,000'], ['الحياة', '0848', '?'], ['الشفاء', '0851', '420,000'], ].map((row, i) => ( {row.map((cell, j) => (
{cell}
))}
))}
⚠ 2 DUPLICATE ENTRIES
{/* Alert */}
إيصالات ورقية قابلة للتعديل؟
); } // ══════════════════════════════════════════════════════════ // SHOT 4: 4-quadrant montage // ══════════════════════════════════════════════════════════ function Shot4_Montage() { const { localTime } = useSprite(); // Question fades up const qT = clamp((localTime - 1.5) / 1.0, 0, 1); const quadrants = [ { label: 'واتساب فوضى', color: FAJR.red }, { label: 'زيارات مزوّرة', color: FAJR.red }, { label: 'إيصالات مزوّرة', color: FAJR.red }, { label: 'إكسل متشابك', color: FAJR.red }, ]; // Exit fade in last 0.5s const exitT = localTime > 4.5 ? (localTime - 4.5) / 0.5 : 0; const exitFade = 1 - exitT; return (
{/* 4 quadrants, desaturated */}
{quadrants.map((q, i) => { const appearT = clamp((localTime - i * 0.2) / 0.5, 0, 1); const shakeAmp = (localTime > 2.5 && localTime < 3.5) ? Math.sin(localTime * 40 + i) * 2 : 0; return (
{i === 0 ? '💬' : i === 1 ? '📍' : i === 2 ? '📄' : '📊'}
{q.label}
); })}
{/* Overlay question at center */}
هل هذا يشبه شركتك؟
); } // ══════════════════════════════════════════════════════════ // SHOT 5: Solution reveal — logo builds // ══════════════════════════════════════════════════════════ function Shot5_Reveal() { const { localTime } = useSprite(); // Gold sweep wipes from right (0 to 0.8s) const sweepT = clamp(localTime / 0.8, 0, 1); // Logo appears at 1.0s const logoT = clamp((localTime - 1.2) / 1.0, 0, 1); const logoEased = Easing.easeOutBack(logoT); // Brand name const nameT = clamp((localTime - 2.5) / 0.8, 0, 1); // Tagline const taglineT = clamp((localTime - 3.5) / 0.8, 0, 1); // Pulse hold const breathe = 1 + Math.sin((localTime - 2) * 2) * 0.015; // Reveal chime at 1.2s const chimeFired = React.useRef(false); React.useEffect(() => { if (localTime > 1.0 && localTime < 1.1 && !chimeFired.current) { chimeFired.current = true; if (window.FajrAudio) window.FajrAudio.play('chime'); } if (localTime < 0.9) chimeFired.current = false; }, [localTime]); // Exit fade const exitT = localTime > 9.0 ? (localTime - 9.0) / 1.0 : 0; const exitFade = 1 - exitT; return (
{/* Gold sweep */} {sweepT < 1 && (
)} {/* Center content */}
{/* Logo */}
logo { e.target.outerHTML = 'ف'; }} />
{/* Brand name */}
فجر ون
{/* Tagline */}
نظام واحد يدير كل شي.
{/* Subtitle with mono label */}
FIELD OPERATIONS · SUITE
); } Object.assign(window, { Shot1_Chaos, Shot2_FakeVisit, Shot3_PaperReceipts, Shot4_Montage, Shot5_Reveal, });