/* Hero Dashboard Graphic - standalone embed
------------------------------------------------------------------
A 1920x1080, 25s seamless-loop animated CRM dashboard. Transparent
background: nothing is painted behind the window, so it sits on any
hero section. No player chrome, no timeline, no controls.
USE - plain HTML page:
USE - Design Component:
The graphic locks a 16:9 box, fills the width of its container and
scales the artwork to fit. Props (all optional):
cursor {boolean} show the pointer default true
float {boolean} slow parallax drift default true
zoom {number} artwork scale, percent default 100
autoplay{boolean} default true
staticT {number} frame held when the user prefers
reduced motion, seconds default 5.2
loop {boolean} default true
Fonts: Inter, Inter Tight, JetBrains Mono. Load them once on the host
page (this file does not inject a stylesheet):
https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=Inter+Tight:wght@400..700&family=JetBrains+Mono:wght@400..600&display=swap
------------------------------------------------------------------ */
/* ── tiny motion runtime (no engine dependency) ───────────────────────── */
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
const Easing = {
linear: (t) => t,
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),
easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2),
easeOutBack: (t) => {
const c1 = 1.70158, c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
},
};
/* piecewise map: input breakpoints -> output values, eased within each span */
function interpolate(input, output, ease) {
const e = ease || Easing.linear;
return (t) => {
if (t <= input[0]) return output[0];
const n = input.length - 1;
if (t >= input[n]) return output[n];
for (let i = 0; i < n; i++) {
if (t >= input[i] && t <= input[i + 1]) {
const span = input[i + 1] - input[i];
const p = span <= 0 ? 1 : (t - input[i]) / span;
return output[i] + (output[i + 1] - output[i]) * e(p);
}
}
return output[n];
};
}
/* one tween as a function of the authored clock */
function animate(o) {
const e = o.ease || Easing.linear;
const span = o.end - o.start;
return (t) => {
if (t <= o.start) return o.from;
if (t >= o.end) return o.to;
return o.from + (o.to - o.from) * e(span <= 0 ? 1 : (t - o.start) / span);
};
}
/* ── the scene list: names + authored durations, in order ─────────────── */
const SCENES = [
{ name: 'Wake', dur: 3.2 },
{ name: 'Raise', dur: 4.2 },
{ name: 'Trend', dur: 3.6 },
{ name: 'Pipeline', dur: 3.6 },
{ name: 'Deals', dur: 3.4 },
{ name: 'Feed', dur: 3.4 },
{ name: 'Close', dur: 1.6 },
{ name: 'Refresh', dur: 2.0 },
];
const CUES = (() => {
const c = {};
let at = 0;
for (const s of SCENES) { c[s.name] = at; at += s.dur; }
return c;
})();
const TOTAL = SCENES.reduce((n, s) => n + s.dur, 0);
const CompositionCtx = React.createContext({ T: 0, CUES, authoredTotal: TOTAL });
const useComposition = () => React.useContext(CompositionCtx);
const C = {
deep: '#14352A', accent: '#CBE81C', tint: '#F7FFD2', deepest: '#6E8511',
ink: '#1C1D1F', ink2: '#232529', ink3: '#505967', ink4: '#6F7988', cap: '#A4ADBA',
w1: '#FFFFFF', w2: '#FAFAFB', w3: '#F3F4F6', w4: '#EDEFF3', w5: '#E4E7EC',
w7: '#D3D8DF', w8: '#CAD0D9', blue: '#266DF0', green: '#0FC27B', amber: '#F5B900',
};
const F = "'Inter', system-ui, sans-serif";
const FT = "'Inter Tight', 'Inter', system-ui, sans-serif";
const FM = "'JetBrains Mono', ui-monospace, monospace";
/* ── three motion helpers, nothing else eases ─────────────────────────── */
const MOTION = {
enter: (o) => animate(Object.assign({ ease: Easing.easeOutCubic }, o)),
draw: (o) => animate(Object.assign({ ease: Easing.easeInOutQuart }, o)),
pop: (o) => animate(Object.assign({ ease: Easing.easeOutBack }, o)),
};
const CANVAS = { w: 1920, h: 1080 };
const DASH = { w: 1370, h: 760 };
const DS = 1.28, DX = 84, DY = 54; // hero dashboard: scale + visual origin
const BASE = { x: DX, y: DY }; // reference for the refresh-sweep mask
const HEADER_H = 66;
/* refresh sweep: front position in stack space, then a 0..1 "data shown" mask */
const sweepX = (T, CUES) => MOTION.draw({ from: -340, to: 2280, start: CUES.Refresh + 1.0, end: CUES.Refresh + 1.85 })(T);
const clearAt = (T, CUES, cx) => clamp((cx - sweepX(T, CUES)) / 160 + 0.5, 0, 1);
function useLife(idx, cx) {
const { T, CUES } = useComposition();
const wake = MOTION.enter({ from: 0, to: 1, start: 0.3 + idx * 0.14, end: 1.25 + idx * 0.14 })(T);
return Math.min(wake, clearAt(T, CUES, cx));
}
/* ── icons ───────────────────────────────────────────────────────────── */
const ICON = {
grid: 'M4 4h6.5v6.5H4zM13.5 4H20v6.5h-6.5zM4 13.5h6.5V20H4zM13.5 13.5H20V20h-6.5z',
bars: 'M4 6h5v13H4zM10.5 9.5h5V19h-5zM17 13h4v6h-4z',
chat: 'M4.5 6.5a2 2 0 012-2h11a2 2 0 012 2v7a2 2 0 01-2 2H10l-5.5 4z',
bank: 'M3.5 9.5L12 4.5l8.5 5M6 10.5v7.5M12 10.5v7.5M18 10.5v7.5M3.5 19.5h17',
doc: 'M6.5 3.5h7L18 8v12H6.5zM13 3.5V8h4.5M9.5 14l2.2 2.2L16 11.9',
people: 'M9.4 11.2a3.3 3.3 0 100-6.6 3.3 3.3 0 000 6.6zM3.6 19.6c0-3 2.6-5.2 5.8-5.2s5.8 2.2 5.8 5.2M16.4 6.4a3 3 0 010 5.6M18 14.9c1.9.7 3.2 2.4 3.2 4.7',
search: 'M11 4.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM15.9 15.9L20 20',
refresh: 'M20 12a8 8 0 11-2.4-5.7M20.2 4.4V10H15',
plus: 'M12 5.5v13M5.5 12h13',
chev: 'M8.5 10.5l3.5 3.5 3.5-3.5',
filter: 'M4.5 6h15l-6.2 7.3v6.2l-2.6-1.7v-4.5z',
check: 'M5.5 12.6l4.4 4.4 8.6-9',
cal: 'M5 6.5h14v13H5zM5 10.6h14M9 4.4v4M15 4.4v4',
trend: 'M4 16.5l4.6-5.2 3.4 3 3.6-6.3 4.4 5',
dots: 'M6 12h.01M12 12h.01M18 12h.01',
sort: 'M7 5.5v13M4 15.5l3 3 3-3M17 18.5v-13M14 8.5l3-3 3 3',
};
function Ico({ d, size = 19, color = C.ink3, sw = 1.7, style }) {
return (
);
}
function Mark({ size = 30, r = 9 }) {
return (