setMenuOpen(false)} />
);
}
// ---------- Hero ----------
function Hero({ accentHue }) {
const [text, setText] = useState("");
const phrases = ["// Unreal · Unity · Godot · Web", "// gameplay · cinematics · tools", "// bilingual FR/EN · Ottawa, ON"];
const [phraseIdx, setPhraseIdx] = useState(0);
useEffect(() => {
let i = 0; let deleting = false;
const full = phrases[phraseIdx];
const tick = () => {
if (!deleting) {
i++;
setText(full.slice(0, i));
if (i >= full.length) { deleting = true; setTimeout(tick, 1800); return; }
} else {
i--;
setText(full.slice(0, i));
if (i <= 0) { setPhraseIdx((p) => (p + 1) % phrases.length); return; }
}
setTimeout(tick, deleting ? 28 : 42);
};
const t = setTimeout(tick, 200);
return () => clearTimeout(t);
}, [phraseIdx]);
return (
Available · Game Dev grad · Algonquin College
Cédrick Picard —
making the half-second
between input and reaction count.
Also known as Nyxoril. I build gameplay, tools and cinematics across
Unreal, Unity and Godot — and I take web-dev side-quests when I need a
break from C++. Advanced Diploma in Game Development from Algonquin College — Dean's List, bilingual FR/EN.
{text}
);
}
function RailItem({ label, value }) {
const ref = useRef(null);
const [shown, setShown] = useState(false);
useEffect(() => {
const node = ref.current; if (!node) return;
const io = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) { setShown(true); io.disconnect(); }
}, { threshold: 0.5 });
io.observe(node);
return () => io.disconnect();
}, []);
const display = useAnimatedValue(value, shown);
return (
);
}
function useAnimatedValue(target, start) {
const [v, setV] = useState(target);
useEffect(() => {
if (!start) { setV(target); return; }
// try to parse numeric prefix; if non-numeric, just set directly
const match = /^(\d+(?:\.\d+)?)(.*)$/.exec(String(target));
if (!match) { setV(target); return; }
const end = parseFloat(match[1]); const suffix = match[2];
const dur = 900; const t0 = performance.now();
let raf;
const tick = (t) => {
const p = Math.min(1, (t - t0) / dur);
const eased = 1 - Math.pow(1 - p, 3);
const cur = end * eased;
const decimals = match[1].includes(".") ? match[1].split(".")[1].length : 0;
setV(cur.toFixed(decimals) + suffix);
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [start, target]);
return v;
}
// ---------- Tech marquee ----------
function TechMarquee() {
const techs = ["C++", "C#", "Unreal Engine 5", "Unity", "Godot", "Node.js", "Three.js", "React", "OpenGL", "GLSL", "Lua", "Python", "Java", "SQLite", "Sequencer", "Blueprints", "Tailwind", "JavaScript"];
return (
{[...techs, ...techs].map((t, i) => (
{t}
))}
);
}
// ---------- Project card ----------
function ProjectCard({ project, onOpen, index }) {
const Cover = window.Covers[project.cover];
const [hover, setHover] = useState(false);
const ref = useRef(null);
const onMove = (e) => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
ref.current.style.setProperty("--mx", `${e.clientX - r.left}px`);
ref.current.style.setProperty("--my", `${e.clientY - r.top}px`);
};
return (
setHover(true)}
onMouseLeave={() => setHover(false)}
onMouseMove={onMove}
onClick={() => onOpen(project.id)}
tabIndex="0"
onKeyDown={(e) => { if (e.key === "Enter") onOpen(project.id); }}
style={{ "--card-hue": project.accentHue, animationDelay: `${index * 80}ms` }}>
{Cover &&
}
{project.status}
{project.year}
{project.youtube && (
video
)}
{project.tagline}
{project.role}
·
{project.engine}
);
}
// ---------- Project modal ----------
function ProjectModal({ project, onClose }) {
const Cover = window.Covers[project.cover];
const [tab, setTab] = useState("overview");
useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
window.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
};
}, [onClose]);
const tabs = ["overview", "highlights", "stack"];
if (project.log && project.log.length) tabs.push("timeline");
return (
e.stopPropagation()} style={{ "--card-hue": project.accentHue }}>
{Cover &&
}
{project.title}
{project.tagline}
{project.status}
{project.year}
{project.role}
{project.engine}
{tabs.map((t) => (
))}
{tab === "overview" && (
{project.note && (
)}
{project.description}
{project.stats.map((s) => (
))}
{project.youtube && (
)}
{project.download && (() => {
const downloads = Array.isArray(project.download) ? project.download : [project.download];
return (
);
})()}
{project.itch && (
)}
)}
{tab === "highlights" && (
{project.highlights.map((h, i) => (
-
{String(i + 1).padStart(2, "0")}
{h}
))}
)}
{tab === "stack" && (
{project.tech.map((t) => (
{t}
))}
)}
{tab === "timeline" && (
{project.log.map((l, i) => (
-
{l.date}
{l.note}
))}
)}
);
}
// ---------- Section header ----------
function SectionHeader({ tag, title, sub }) {
return (
[ {tag} ]
{title}
{sub && {sub}
}
);
}
// ---------- Work section ----------
function WorkSection({ projects, onOpen }) {
const [filter, setFilter] = useState("all");
const statuses = Array.from(new Set(projects.map((p) => p.status.toLowerCase())));
const filters = ["all", ...statuses];
const filtered = projects.filter((p) => filter === "all" || p.status.toLowerCase() === filter);
return (
{filters.map((f) => (
))}
{filtered.map((p, i) => (
))}
);
}
// ---------- About + timeline ----------
function AboutSection({ timeline, certifications }) {
return (
I'm Cédrick — a recent Game Development grad from Algonquin College (Dean's List, GPA 3.18).
My capstone year was spent inside the college's Capstone Studio as part of team CtrlAltElite,
shipping a game we showed publicly every week at the Canadian Aviation and Space Museum.
My focus is the things players feel but never name — character movement, UI responsiveness,
the half-second between input and reaction. I work across Unreal C++ & Blueprints, Unity, Godot,
and I take web-dev side-quests in Node.js and Three.js to keep the other half of my brain sharp.
Bilingual (FR/EN). Based in Ottawa / North Bay, ON. I worked as a McDonald's crew trainer for nearly three years and a mover for a season — neither was glamorous, but both were good schools for showing up on time and finishing what you start.
Certifications
{certifications.map((c, i) => (
-
{c.year}
{c.issuer}
{c.name}
))}
{timeline.map((t, i) => (
-
{t.year}
{t.role}
{t.where}
{t.note}
))}
);
}
// ---------- Stack (periodic-table style tiles) ----------
function StackTile({ item, hue, index }) {
const ref = useRef(null);
const onMove = (e) => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
const x = ((e.clientX - r.left) / r.width - 0.5) * 12;
const y = ((e.clientY - r.top) / r.height - 0.5) * -12;
ref.current.style.setProperty("--rx", `${y}deg`);
ref.current.style.setProperty("--ry", `${x}deg`);
ref.current.style.setProperty("--mx", `${e.clientX - r.left}px`);
ref.current.style.setProperty("--my", `${e.clientY - r.top}px`);
};
const onLeave = () => {
if (!ref.current) return;
ref.current.style.setProperty("--rx", `0deg`);
ref.current.style.setProperty("--ry", `0deg`);
};
return (
{String(index + 1).padStart(2, "0")}
{item.abbr}
{item.name}
);
}
function StackSection({ skills }) {
// distinct hue per group
const groupHues = [220, 168, 12, 285];
return (
{skills.map((group, gi) => (
0{gi + 1}
{group.group.toLowerCase()}
{group.items.length} items
{group.items.map((item, i) => (
))}
))}
);
}
// ---------- Contact ----------
function ContactSection({ accentHue }) {
return (
);
}
window.PortfolioParts = {
GridBackground, Nav, Hero, ProjectCard, ProjectModal, TechMarquee,
WorkSection, AboutSection, StackSection, ContactSection
};
window.scrollToId = scrollToId;
window.smoothScrollTo = smoothScrollTo;