chore: ai, public course and other ui improvements

This commit is contained in:
Rotimi Best
2026-05-09 06:05:03 +01:00
parent 5717dd8004
commit c31ad63955
318 changed files with 20668 additions and 4788 deletions
@@ -35,7 +35,7 @@
});
</script>
<AnimatePresence let:item list={[{ key: id }]}>
<AnimatePresence let:_item list={[{ key: id }]}>
<Motion
initial="hidden"
animate={isInView}
@@ -0,0 +1,287 @@
<script module lang="ts">
const TWO_PI = Math.PI * 2;
/** Resolves `color-mix(in srgb, var(--primary) …)` for canvas dot fills (inherits `--primary` from this component’s DOM subtree). */
function primaryMixOnRoot(el: HTMLElement, primaryPercent: number): string {
const probe = document.createElement('span');
probe.setAttribute('aria-hidden', 'true');
probe.style.cssText =
'position:fixed;left:0;top:0;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;' +
`color:var(--primary)`;
el.appendChild(probe);
const resolved = getComputedStyle(probe).color;
probe.remove();
return resolved || 'rgba(128, 128, 128, 0.25)';
}
type Dot = {
ax: number;
ay: number;
sx: number;
sy: number;
vx: number;
vy: number;
x: number;
y: number;
};
</script>
<script lang="ts">
import { cn } from '../../../tools';
type Props = {
dotRadius?: number;
dotSpacing?: number;
cursorRadius?: number;
cursorForce?: number;
bulgeOnly?: boolean;
bulgeStrength?: number;
glowRadius?: number;
sparkle?: boolean;
waveAmplitude?: number;
glowColor?: string;
class?: string;
};
let {
dotRadius = 1.5,
dotSpacing = 14,
cursorRadius = 500,
cursorForce = 0.1,
bulgeOnly = true,
bulgeStrength = 67,
glowRadius = 160,
sparkle = false,
waveAmplitude = 0,
glowColor = '#ffffff',
class: className = ''
}: Props = $props();
let root: HTMLDivElement | undefined;
let canvas: HTMLCanvasElement | undefined;
let glowEl: SVGCircleElement | undefined;
const glowId = `dot-field-glow-${crypto.randomUUID().slice(0, 10)}`;
let dots: Dot[] = [];
const mouse = { x: -9999, y: -9999, prevX: -9999, prevY: -9999, speed: 0 };
let size = { w: 0, h: 0, offsetX: 0, offsetY: 0 };
let glowOpacity = 0;
let engagement = 0;
$effect(() => {
void dotRadius;
void dotSpacing;
void cursorRadius;
void cursorForce;
void bulgeOnly;
void bulgeStrength;
void sparkle;
void waveAmplitude;
void glowColor;
const currentGlow = glowEl;
if (!canvas || !root) return;
const canvasEl: HTMLCanvasElement = canvas;
const rootEl: HTMLDivElement = root;
const fillGradientFrom = primaryMixOnRoot(rootEl, 40);
const fillGradientTo = primaryMixOnRoot(rootEl, 22);
const ctx = canvasEl.getContext('2d', { alpha: true });
if (!ctx) return;
const context = ctx;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let resizeTimer: ReturnType<typeof setTimeout>;
let raf = 0;
let frameCount = 0;
function buildDots(w: number, h: number) {
const step = dotRadius + dotSpacing;
const cols = Math.floor(w / step);
const rows = Math.floor(h / step);
const padX = (w % step) / 2;
const padY = (h % step) / 2;
const nextDots: Dot[] = new Array(rows * cols);
let idx = 0;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const ax = padX + col * step + step / 2;
const ay = padY + row * step + step / 2;
nextDots[idx++] = { ax, ay, sx: ax, sy: ay, vx: 0, vy: 0, x: ax, y: ay };
}
}
dots = nextDots;
}
function doResize() {
const rect = rootEl.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
canvasEl.width = w * dpr;
canvasEl.height = h * dpr;
canvasEl.style.width = `${w}px`;
canvasEl.style.height = `${h}px`;
context.setTransform(dpr, 0, 0, dpr, 0, 0);
size = {
w,
h,
offsetX: rect.left + window.scrollX,
offsetY: rect.top + window.scrollY
};
buildDots(w, h);
}
function resize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(doResize, 100);
}
function onMouseMove(e: MouseEvent) {
mouse.x = e.pageX - size.offsetX;
mouse.y = e.pageY - size.offsetY;
}
function updateMouseSpeed() {
const dx = mouse.prevX - mouse.x;
const dy = mouse.prevY - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
mouse.speed += (dist - mouse.speed) * 0.5;
if (mouse.speed < 0.001) mouse.speed = 0;
mouse.prevX = mouse.x;
mouse.prevY = mouse.y;
}
const speedInterval = setInterval(updateMouseSpeed, 20);
function tick() {
frameCount++;
const len = dots.length;
const { w, h } = size;
const t = frameCount * 0.02;
const targetEngagement = Math.min(mouse.speed / 5, 1);
engagement += (targetEngagement - engagement) * 0.06;
if (engagement < 0.001) engagement = 0;
glowOpacity += (engagement - glowOpacity) * 0.08;
if (currentGlow) {
currentGlow.setAttribute('cx', String(mouse.x));
currentGlow.setAttribute('cy', String(mouse.y));
currentGlow.style.opacity = String(glowOpacity);
}
context.clearRect(0, 0, w, h);
const grad = context.createLinearGradient(0, 0, w, h);
grad.addColorStop(0, fillGradientFrom);
grad.addColorStop(1, fillGradientTo);
context.fillStyle = grad;
const crSq = cursorRadius * cursorRadius;
const rad = dotRadius / 2;
context.beginPath();
for (let i = 0; i < len; i++) {
const d = dots[i];
const dx = mouse.x - d.ax;
const dy = mouse.y - d.ay;
const distSq = dx * dx + dy * dy;
if (distSq < crSq && engagement > 0.01) {
const dist = Math.sqrt(distSq);
const angle = Math.atan2(dy, dx);
if (bulgeOnly) {
const falloff = 1 - dist / cursorRadius;
const push = falloff * falloff * bulgeStrength * engagement;
d.sx += (d.ax - Math.cos(angle) * push - d.sx) * 0.15;
d.sy += (d.ay - Math.sin(angle) * push - d.sy) * 0.15;
} else {
const safeDist = Math.max(dist, 0.001);
const move = (500 / safeDist) * (mouse.speed * cursorForce);
d.vx += Math.cos(angle) * -move;
d.vy += Math.sin(angle) * -move;
}
} else if (bulgeOnly) {
d.sx += (d.ax - d.sx) * 0.1;
d.sy += (d.ay - d.sy) * 0.1;
}
if (!bulgeOnly) {
d.vx *= 0.9;
d.vy *= 0.9;
d.x = d.ax + d.vx;
d.y = d.ay + d.vy;
d.sx += (d.x - d.sx) * 0.1;
d.sy += (d.y - d.sy) * 0.1;
}
let drawX = d.sx;
let drawY = d.sy;
if (waveAmplitude > 0) {
drawY += Math.sin(d.ax * 0.03 + t) * waveAmplitude;
drawX += Math.cos(d.ay * 0.03 + t * 0.7) * waveAmplitude * 0.5;
}
if (sparkle) {
const hash = ((i * 2654435761) ^ (frameCount >> 3)) >>> 0;
if (hash % 100 < 3) {
context.moveTo(drawX + rad * 1.8, drawY);
context.arc(drawX, drawY, rad * 1.8, 0, TWO_PI);
} else {
context.moveTo(drawX + rad, drawY);
context.arc(drawX, drawY, rad, 0, TWO_PI);
}
} else {
context.moveTo(drawX + rad, drawY);
context.arc(drawX, drawY, rad, 0, TWO_PI);
}
}
context.fill();
raf = requestAnimationFrame(tick);
}
doResize();
window.addEventListener('resize', resize);
window.addEventListener('mousemove', onMouseMove, { passive: true });
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
clearInterval(speedInterval);
clearTimeout(resizeTimer);
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', onMouseMove);
};
});
</script>
<div bind:this={root} class={cn('relative h-full w-full', className)}>
<canvas bind:this={canvas} class="absolute inset-0 h-full w-full"></canvas>
<svg class="pointer-events-none absolute inset-0 h-full w-full" aria-hidden="true">
<defs>
<radialGradient id={glowId}>
<stop offset="0%" stop-color="#fff" />
<stop offset="100%" stop-color="#fff" />
</radialGradient>
</defs>
<circle
bind:this={glowEl}
cx="-9999"
cy="-9999"
r={glowRadius}
fill="url(#{glowId})"
style:opacity="0"
style:will-change="opacity"
/>
</svg>
</div>
@@ -0,0 +1 @@
export { default as DotField } from './dot-field.svelte';
@@ -31,7 +31,7 @@
</script>
<div class={cn('flex justify-center space-x-1', containerClass)}>
<AnimatePresence let:item list={[{ key: wordsspilit }]}>
<AnimatePresence let:_item list={[{ key: wordsspilit }]}>
{#each wordsspilit as char, i}
<Motion
initial="hidden"
@@ -1,4 +1,5 @@
export * from './border-beam';
export * from './dot-field';
export * from './dot-pattern';
export * from './card';
export * from './blurfade';