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