main
tsx 319 lines 7.68 KB
Raw
1 import React, {useMemo} from 'react';
2 import {
3 AbsoluteFill,
4 interpolate,
5 random,
6 spring,
7 useCurrentFrame,
8 useVideoConfig,
9 } from 'remotion';
10 import {
11 Database,
12 Gauge,
13 GitBranch,
14 LayoutDashboard,
15 Logs,
16 Server,
17 ShieldCheck,
18 } from 'lucide-react';
19
20 type NodeSpec = {
21 id: string;
22 label: string;
23 Icon: React.FC<{size?: number; color?: string}>;
24 angleDeg: number;
25 };
26
27 const BG = '#0b0912';
28 const PRIMARY = '#f85d3b';
29 const ACCENT = '#884fff';
30
31 const NodeCard: React.FC<{
32 cx: number;
33 cy: number;
34 label: string;
35 Icon: NodeSpec['Icon'];
36 accent: string;
37 appear: number;
38 }> = ({cx, cy, label, Icon, accent, appear}) => {
39 const frame = useCurrentFrame();
40 const {fps} = useVideoConfig();
41 const s = spring({frame, fps, config: {damping: 14, mass: 0.8, stiffness: 120}});
42 const scale = interpolate(s, [0, 1], [0.92, 1]) * appear;
43 const opacity = interpolate(s, [0, 1], [0, 1]) * appear;
44
45 return (
46 <div
47 style={{
48 position: 'absolute',
49 left: cx,
50 top: cy,
51 transform: `translate(-50%, -50%) scale(${scale})`,
52 opacity,
53 width: 180,
54 padding: '14px 14px 12px',
55 borderRadius: 16,
56 background: 'rgba(255,255,255,0.03)',
57 border: '1px solid rgba(255,255,255,0.08)',
58 backdropFilter: 'blur(8px)',
59 boxShadow: `0 0 24px rgba(136,79,255,0.08)`,
60 textAlign: 'center',
61 fontFamily:
62 'IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial',
63 }}
64 >
65 <div
66 style={{
67 display: 'inline-flex',
68 alignItems: 'center',
69 justifyContent: 'center',
70 width: 58,
71 height: 58,
72 borderRadius: 18,
73 background: 'rgba(255,255,255,0.02)',
74 border: `1px solid rgba(255,255,255,0.10)`,
75 boxShadow: `0 0 18px ${accent}33`,
76 }}
77 >
78 <Icon size={30} color={accent} />
79 </div>
80 <div style={{marginTop: 10, fontSize: 16, fontWeight: 600, color: 'rgba(255,255,255,0.92)'}}>
81 {label}
82 </div>
83 </div>
84 );
85 };
86
87 const Stream: React.FC<{
88 x1: number;
89 y1: number;
90 x2: number;
91 y2: number;
92 color: string;
93 seed: number;
94 }> = ({x1, y1, x2, y2, color, seed}) => {
95 const frame = useCurrentFrame();
96 const {durationInFrames} = useVideoConfig();
97
98 // Dotted stream
99 const dash = 10;
100 const gap = 8;
101 const speed = 2.0 + random(seed) * 1.4;
102 const offset = (frame * speed) % (dash + gap);
103
104 // Occasional packet pulse (repeatable)
105 const t = (frame + Math.floor(random(seed + 1) * 60)) % durationInFrames;
106 const pulseEvery = 45 + Math.floor(random(seed + 2) * 35);
107 const pulsePhase = t % pulseEvery;
108 const pulseT = pulsePhase / pulseEvery;
109 const packetVisible = pulsePhase < 18;
110
111 // Quadratic curve control point (bend outward)
112 const mx = (x1 + x2) / 2;
113 const my = (y1 + y2) / 2;
114 const bend = 80;
115 const cx = mx + (my - 300) * 0.15;
116 const cy = my - bend;
117
118 const path = `M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`;
119
120 // Packet position along curve (approx via linear interpolation on t; fine for small effect)
121 const px = x1 + (x2 - x1) * pulseT;
122 const py = y1 + (y2 - y1) * pulseT;
123
124 return (
125 <svg
126 style={{
127 position: 'absolute',
128 left: 0,
129 top: 0,
130 width: '100%',
131 height: '100%',
132 overflow: 'visible',
133 }}
134 >
135 <path
136 d={path}
137 fill="none"
138 stroke={color}
139 strokeWidth={2.2}
140 strokeDasharray={`${dash} ${gap}`}
141 strokeDashoffset={-offset}
142 opacity={0.45}
143 style={{
144 filter: `drop-shadow(0 0 10px ${color}55) drop-shadow(0 0 18px ${color}33)`,
145 }}
146 />
147 {packetVisible ? (
148 <circle
149 cx={px}
150 cy={py}
151 r={4.2}
152 fill={color}
153 opacity={0.9}
154 style={{filter: `drop-shadow(0 0 14px ${color})`}}
155 />
156 ) : null}
157 </svg>
158 );
159 };
160
161 export const HubSpoke: React.FC = () => {
162 const frame = useCurrentFrame();
163 const {fps, durationInFrames, width, height} = useVideoConfig();
164
165 const center = {x: width / 2, y: height / 2};
166 const radius = 210;
167
168 const nodes: NodeSpec[] = useMemo(
169 () => [
170 { id: 'wazuh-manager', label: 'Wazuh Manager', Icon: Server, angleDeg: -90 },
171 { id: 'wazuh-indexer', label: 'Wazuh Indexer', Icon: Database, angleDeg: -30 },
172 { id: 'graylog', label: 'Graylog', Icon: Logs, angleDeg: 30 },
173 { id: 'grafana', label: 'Grafana', Icon: Gauge, angleDeg: 90 },
174 { id: 'shuffle', label: 'Shuffle', Icon: GitBranch, angleDeg: 150 },
175 { id: 'velociraptor', label: 'Velociraptor', Icon: LayoutDashboard, angleDeg: 210 },
176 ],
177 []
178 );
179
180 const appear = spring({frame, fps, config: {damping: 16, stiffness: 120, mass: 0.9}});
181
182 const hubPulse = 0.5 + 0.5 * Math.sin((frame / durationInFrames) * Math.PI * 2);
183 const hubGlow = interpolate(hubPulse, [0, 1], [0.25, 0.6]);
184
185 return (
186 <AbsoluteFill
187 style={{
188 backgroundColor: BG,
189 overflow: 'hidden',
190 }}
191 >
192 {/* Background: subtle grid + gradient */}
193 <AbsoluteFill
194 style={{
195 background:
196 `radial-gradient(900px 380px at 50% 30%, rgba(136,79,255,0.18), transparent 60%),` +
197 `radial-gradient(700px 420px at 70% 70%, rgba(248,93,59,0.12), transparent 55%),` +
198 `linear-gradient(180deg, rgba(255,255,255,0.02), transparent 40%)`,
199 }}
200 />
201 <AbsoluteFill
202 style={{
203 backgroundImage:
204 'linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px)',
205 backgroundSize: '56px 56px',
206 opacity: 0.10,
207 transform: `translateY(${interpolate(
208 (frame % durationInFrames) / durationInFrames,
209 [0, 1],
210 [0, -56]
211 )}px)`,
212 }}
213 />
214
215 {/* Streams */}
216 {nodes.map((n, idx) => {
217 const a = (n.angleDeg * Math.PI) / 180;
218 const x = center.x + Math.cos(a) * radius;
219 const y = center.y + Math.sin(a) * radius;
220 const color = idx % 2 === 0 ? PRIMARY : ACCENT;
221 return (
222 <Stream
223 key={n.id}
224 x1={x}
225 y1={y}
226 x2={center.x}
227 y2={center.y}
228 color={color}
229 seed={idx + 10}
230 />
231 );
232 })}
233
234 {/* Hub */}
235 <div
236 style={{
237 position: 'absolute',
238 left: center.x,
239 top: center.y,
240 transform: `translate(-50%, -50%) scale(${0.98 + appear * 0.02})`,
241 width: 320,
242 padding: '22px 18px 18px',
243 borderRadius: 22,
244 background: 'rgba(255,255,255,0.04)',
245 border: '1px solid rgba(255,255,255,0.10)',
246 textAlign: 'center',
247 boxShadow: `0 0 35px rgba(248,93,59,${hubGlow}), 0 0 55px rgba(136,79,255,0.18)`,
248 backdropFilter: 'blur(10px)',
249 fontFamily:
250 'IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial',
251 }}
252 >
253 <div
254 style={{
255 display: 'inline-flex',
256 alignItems: 'center',
257 justifyContent: 'center',
258 width: 76,
259 height: 76,
260 borderRadius: 22,
261 background: 'rgba(255,255,255,0.02)',
262 border: '1px solid rgba(255,255,255,0.12)',
263 boxShadow: `0 0 22px ${PRIMARY}55`,
264 }}
265 >
266 <ShieldCheck size={38} color={PRIMARY} />
267 </div>
268 <div
269 style={{
270 marginTop: 12,
271 fontSize: 22,
272 fontWeight: 700,
273 color: 'rgba(255,255,255,0.94)',
274 letterSpacing: 0.2,
275 }}
276 >
277 SOCFortress CoPilot
278 </div>
279 <div
280 style={{
281 marginTop: 6,
282 fontSize: 15,
283 color: 'rgba(255,255,255,0.70)',
284 }}
285 >
286 Connect • Enrich • Automate
287 </div>
288 </div>
289
290 {/* Outer nodes */}
291 {nodes.map((n, idx) => {
292 const a = (n.angleDeg * Math.PI) / 180;
293 const x = center.x + Math.cos(a) * radius;
294 const y = center.y + Math.sin(a) * radius;
295 const accent = idx % 2 === 0 ? PRIMARY : ACCENT;
296 return (
297 <NodeCard
298 key={n.id}
299 cx={x}
300 cy={y}
301 label={n.label}
302 Icon={n.Icon}
303 accent={accent}
304 appear={appear}
305 />
306 );
307 })}
308
309 {/* Subtle vignette */}
310 <AbsoluteFill
311 style={{
312 background:
313 'radial-gradient(circle at 50% 50%, transparent 40%, rgba(0,0,0,0.55) 85%)',
314 opacity: 0.55,
315 }}
316 />
317 </AbsoluteFill>
318 );
319 };