// 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 */}
{/* Clinic pin (where visit was claimed) — top-right */}
{/* Dashed red line between */}
{/* 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 */}

{ 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,
});