// Main app - pulls all sections together, sets accent color via Tweaks. const { useEffect, useState, useRef, useCallback } = React; function LanguageDropdown({ lang, setLang }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); const opts = [ { v: "en", short: "EN", long: "English" }, { v: "nl", short: "NL", long: "Nederlands" }, ]; const current = opts.find((o) => o.v === lang) || opts[0]; return (
{open && ( )}
); } // Streak palette and layout helper. Streaks always start at heroEnd (just // past the helmet zoom) and fall the full remaining document height. const STREAK_PALETTE = ["#ff7a00", "#ffa040", "#ffd166", "#00d9ff", "#7ae6ff"]; function getLayout() { const vh = window.innerHeight; // Spawn well inside the helmet's sticky coverage (viewport 0–100vh) so new // streaks are fully hidden by the helmet image and only emerge as they // fall past it. Fall distance grows accordingly so streaks still reach // the bottom of the document. const heroEnd = vh * 0.5; const docHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, ); const fallDistance = Math.max(vh * 6, docHeight - heroEnd); return { heroEnd, fallDistance }; } // Minimum horizontal separation (in percent of container width) between any // two streaks. With 8 slots and 100% width, an even distribution lands at // 12.5% gaps - leaving us comfortable headroom at 10%. const MIN_LEFT_GAP = 10; function pickSpacedLeft(existingLefts) { // Try a handful of random samples, accept the first one that clears the // gap threshold. If none does, fall back to the candidate with the // largest minimum distance - keeps streaks as far apart as possible even // when the slot pool is already saturated. let bestLeft = Math.random() * 100; let bestMinDist = -Infinity; for (let i = 0; i < 30; i++) { const candidate = Math.random() * 100; let minDist = Infinity; for (const ex of existingLefts) { const d = Math.abs(candidate - ex); if (d < minDist) minDist = d; } if (existingLefts.length === 0 || minDist >= MIN_LEFT_GAP) { return candidate; } if (minDist > bestMinDist) { bestMinDist = minDist; bestLeft = candidate; } } return bestLeft; } function makeStreakConfig({ initial, existingLefts = [] }) { // Streaks now fall at a constant px/s rate; the Streak component reads the // current fallDistance every frame via a ref, so a streak in flight when // the document grows (images/fonts loaded late) automatically continues // until it reaches the new bottom rather than despawning mid-page. return { id: Math.random().toString(36).slice(2), left: pickSpacedLeft(existingLefts), color: STREAK_PALETTE[Math.floor(Math.random() * STREAK_PALETTE.length)], // One unified rate for every streak (initial wave + replacements). An // earlier version used 150 px/s for the first wave and 380 px/s for // steady-state, but when the slow wave finished the rate jumped 2.5x // all at once - users noticed it as the streaks "suddenly going fast." // Constant 200 px/s sidesteps the discontinuity entirely. fallRate: 200, // Positive delays stagger the initial wave so streaks emerge one by one // from the top instead of appearing already mid-flight. delay: initial ? Math.random() * 8 : 0, height: 70 + Math.random() * 140, }; } // Single streak - animated by requestAnimationFrame in JS. We previously // used a CSS animation with `translateY(var(--fall))`, but the @property + // custom-property interpolation is unreliable across browsers (in some // browsers the transform snaps from 0 to the final value with no visible // fall, which made streaks appear and then "disappear" before reaching the // user). JS animation is bulletproof: every frame, we set the transform // based on real elapsed time, and the streak truly travels from heroEnd // down to docBottom. const Streak = React.memo(function Streak({ slot, cfg, heroEndPx, fallDistance, onDone }) { const ref = useRef(null); // Live fallDistance ref so the animate loop reads the latest value every // frame. Prevents existing streaks from despawning at a stale boundary if // the document grows mid-flight (images/fonts/lazy content), and avoids // restarting the RAF when the prop changes. const fallDistanceRef = useRef(fallDistance); useEffect(() => { fallDistanceRef.current = fallDistance; }, [fallDistance]); useEffect(() => { let raf = 0; let cancelled = false; // Per-frame delta accumulation rather than (now - startTime). When the // tab is hidden, RAF pauses but performance.now() keeps advancing - if // we computed total elapsed on resume, every streak would teleport past // the bottom and fire onDone in unison, spawning a synchronized burst // of replacements. Clamping dt to 100ms makes the streak effectively // pause while the tab is away and resume from where it left off. let lastTs = null; let waitedMs = 0; let fallen = 0; const totalDelayMs = cfg.delay * 1000; if (ref.current) { ref.current.style.transform = "translateY(0px)"; } const animate = (now) => { if (cancelled) return; if (lastTs !== null) { const dt = Math.min(now - lastTs, 100); if (waitedMs < totalDelayMs) { waitedMs += dt; } else { fallen += (dt / 1000) * cfg.fallRate; } } lastTs = now; const fd = fallDistanceRef.current; if (ref.current) { ref.current.style.transform = `translateY(${Math.min(fallen, fd)}px)`; } if (fallen < fd) { raf = requestAnimationFrame(animate); } else { onDone(slot); } }; raf = requestAnimationFrame(animate); return () => { cancelled = true; if (raf) cancelAnimationFrame(raf); }; }, [slot, cfg.id, cfg.fallRate, cfg.delay, onDone]); return ( ); }); // Visor exhaust - 5 streaks falling from heroEnd all the way down to the // bottom of the document. Each is animated by its own RAF loop. After the // fall completes naturally, the slot's React key changes and a new streak // mounts to start the journey over. function VisorExhaust() { const containerRef = useRef(null); const [layout, setLayout] = useState(getLayout); const [slots, setSlots] = useState(() => { // Build the initial 8 sequentially so each new streak knows the lefts // already taken and can pick a spaced-out position. const arr = []; for (let i = 0; i < 8; i++) { arr.push(makeStreakConfig({ initial: true, existingLefts: arr.map((s) => s.left), })); } return arr; }); const replaceSlot = useCallback((slot) => { setSlots((prev) => { const next = [...prev]; const others = prev.filter((_, i) => i !== slot).map((s) => s.left); next[slot] = makeStreakConfig({ initial: false, existingLefts: others }); return next; }); }, []); useEffect(() => { const remeasure = () => setLayout(getLayout()); const onScroll = () => { const el = containerRef.current; if (!el) return; const y = window.scrollY; const vh = window.innerHeight; // Trigger fade-in over the last bit of the hero so streaks become // visible right as the helmet exits the viewport. const start = vh * 1.7; const end = vh * 2; el.style.opacity = String(Math.max(0, Math.min(1, (y - start) / (end - start)))); }; const onResize = () => { remeasure(); onScroll(); }; onScroll(); remeasure(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onResize); window.addEventListener("load", remeasure); // The document keeps growing after initial mount as images, fonts, and // late-laying-out sections settle. Without re-measuring, the streaks' // fallDistance freezes at the early-mount value and they despawn mid- // page (around the Activities section) instead of reaching the footer. let ro = null; if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(remeasure); ro.observe(document.body); } document.fonts?.ready?.then(remeasure).catch(() => {}); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onResize); window.removeEventListener("load", remeasure); if (ro) ro.disconnect(); }; }, []); return ( ); } function App() { const [tweaks, setTweak] = window.useTweaks(window.TWEAK_DEFAULTS); const [lang, setLang] = useState("en"); const [navOpen, setNavOpen] = useState(false); useEffect(() => { const root = document.documentElement; // Validate as a hex color before applying so a malicious tweak edit // can't smuggle arbitrary CSS into a `var(--accent)` consumer. const isHex = (s) => typeof s === "string" && /^#[0-9a-f]{3,8}$/i.test(s); if (isHex(tweaks.accent)) root.style.setProperty("--accent", tweaks.accent); if (isHex(tweaks.accent2)) root.style.setProperty("--accent-2", tweaks.accent2); }, [tweaks.accent, tweaks.accent2]); const closeNav = () => setNavOpen(false); return ( <>
© 2026 · Martijn Van Baelen · I-Talent Portfolio
setTweak("accent", v)} /> setTweak("accent2", v)} /> { setTweak("palette", v); const presets = { redblue: { accent: "#ff2e3e", accent2: "#00d9ff" }, orange: { accent: "#ff7a00", accent2: "#ffd400" }, violet: { accent: "#b14bff", accent2: "#00ffa8" }, mono: { accent: "#e8edf5", accent2: "#6f7c8e" }, }; const p = presets[v]; if (p) setTweak(p); }} options={[ { label: "Red/Blue", value: "redblue" }, { label: "Orange", value: "orange" }, { label: "Violet", value: "violet" }, { label: "Mono", value: "mono" }, ]} /> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();