// Shared components used across all scenes. // ─────────────────── PALETTE ─────────────────── const FAJR = { navy: '#1a2f3f', navy900: '#0f1f2b', navy800: '#152633', navy700: '#22404f', gold: '#d4a853', goldSoft: '#e6c687', goldDim: '#8a6d34', ivory: '#f5f1e8', paper: '#fafaf7', ink: '#14222c', muted: '#5b6d78', green: '#7fc18a', red: '#e85d5d', blue: '#5b9bd5', orange: '#e8a44d', line: 'rgba(212,168,83,.22)', lineDark: 'rgba(245,241,232,.08)', }; // ─────────────────── SCENE BG ─────────────────── // Dark navy background with grid + two glows, reused in every scene. function SceneBG({ showGrid = true }) { return ( <>
{showGrid && (
)}
); } // ─────────────────── SCENE HEADER ─────────────────── // Top-left: slide number + title caption. Animates in. function SceneHeader({ number, label, title, desc }) { const { localTime } = useSprite(); const fadeIn = Easing.easeOutCubic(clamp(localTime / 0.6, 0, 1)); const ty = (1 - fadeIn) * 14; return (
{number} — {label}

{title}

{desc && (

{desc}

)}
); } // ─────────────────── MOUSE CURSOR ─────────────────── // Animates from (x1,y1) to (x2,y2) over [start, moveEnd], then fades at fadeStart. // Triggers a click ripple + tap sound at moveEnd. function Cursor({ x1, y1, x2, y2, start = 0, moveEnd = 1, fadeStart = null, clickAt = null }) { const { localTime } = useSprite(); if (localTime < start) return null; const moveT = clamp((localTime - start) / (moveEnd - start), 0, 1); const eased = Easing.easeInOutCubic(moveT); const x = x1 + (x2 - x1) * eased; const y = y1 + (y2 - y1) * eased; let opacity = 1; if (fadeStart != null && localTime >= fadeStart) { opacity = clamp(1 - (localTime - fadeStart) / 0.3, 0, 1); } const actualClickAt = clickAt != null ? clickAt : moveEnd; const sinceClick = localTime - actualClickAt; const showRipple = sinceClick >= 0 && sinceClick < 0.5; const rippleScale = showRipple ? 1 + sinceClick * 3 : 0; const rippleOpacity = showRipple ? 1 - (sinceClick / 0.5) : 0; // Fire tap sound on click frame (approx) const firedRef = React.useRef(false); React.useEffect(() => { if (sinceClick >= 0 && sinceClick < 0.08 && !firedRef.current) { firedRef.current = true; if (window.FajrAudio) window.FajrAudio.play('tap'); } if (sinceClick < -0.05) firedRef.current = false; }, [sinceClick]); return ( <> {showRipple && (
)} ); } // ─────────────────── DEVICE FRAMES ─────────────────── // Phone frame — positioned inside the scene. function PhoneFrame({ x, y, width = 320, children, entryStart = 0 }) { const { localTime } = useSprite(); const t = clamp((localTime - entryStart) / 0.7, 0, 1); const eased = Easing.easeOutBack(t); const scale = 0.7 + 0.3 * eased; const opacity = t; return (
{children}
); } // Desktop frame. function DesktopFrame({ x, y, width = 780, url = 'fajrone.iq', children, entryStart = 0 }) { const { localTime } = useSprite(); const t = clamp((localTime - entryStart) / 0.7, 0, 1); const eased = Easing.easeOutCubic(t); const scale = 0.9 + 0.1 * eased; const opacity = t; return (
{url}
{children}
); } // ─────────────────── MOCK ELEMENTS ─────────────────── function MockRow({ avatar, name, sub, badge, badgeColor = 'gold', appearAt = 0, children }) { const { localTime } = useSprite(); const t = clamp((localTime - appearAt) / 0.45, 0, 1); const eased = Easing.easeOutCubic(t); const opacity = t; const tx = (1 - eased) * -20; // play soft-tick on appearance const firedRef = React.useRef(false); React.useEffect(() => { if (localTime >= appearAt && localTime < appearAt + 0.1 && !firedRef.current) { firedRef.current = true; if (window.FajrAudio) window.FajrAudio.play('soft-tick'); } if (localTime < appearAt - 0.05) firedRef.current = false; }, [localTime, appearAt]); const badgeColors = { gold: { bg: 'rgba(212,168,83,.15)', color: FAJR.gold }, green: { bg: 'rgba(127,193,138,.15)', color: FAJR.green }, blue: { bg: 'rgba(91,155,213,.15)', color: FAJR.blue }, orange: { bg: 'rgba(232,164,77,.15)', color: FAJR.orange }, red: { bg: 'rgba(232,93,93,.15)', color: FAJR.red }, }; const bc = badgeColors[badgeColor] || badgeColors.gold; return (
{avatar && (
{avatar}
)}
{name}
{sub &&
{sub}
}
{badge && ( {badge} )} {children}
); } function MockHeader({ title, badge, badgeColor }) { const badgeColors = { gold: { bg: 'rgba(212,168,83,.15)', color: FAJR.gold }, green: { bg: 'rgba(127,193,138,.15)', color: FAJR.green }, blue: { bg: 'rgba(91,155,213,.15)', color: FAJR.blue }, orange: { bg: 'rgba(232,164,77,.15)', color: FAJR.orange }, red: { bg: 'rgba(232,93,93,.15)', color: FAJR.red }, }; const bc = badgeColors[badgeColor] || badgeColors.gold; return (
{title} {badge && ( {badge} )}
); } // ─────────────────── SCENE TRANSITION OVERLAY ─────────────────── // Gold sweep between scenes. function SceneTransition({ at, duration = 0.6 }) { const { localTime } = useSprite(); const dt = localTime - at; if (dt < 0 || dt > duration) return null; const t = dt / duration; // Sweep from right to left (RTL direction) const x = (1 - t) * 100; // 100% -> 0% return (
); } // Export all to window so other scripts can use Object.assign(window, { FAJR, SceneBG, SceneHeader, Cursor, PhoneFrame, DesktopFrame, MockRow, MockHeader, SceneTransition, });