MAC / frontend /src /lib /components /PretextBackground.svelte
Aaryan17's picture
chore: upload MAC codebase to HF Space
0e76632 verified
<!-- PretextBackground.svelte Pretext-inspired dynamic floating text background
Words (MAC, MBM, AI) float and scatter on hover/touch with physics -->
<script>
import { onMount, onDestroy } from 'svelte';
let containerEl;
let rafId;
let words = [];
let mouse = { x: -9999, y: -9999, active: false };
// Word configuration scattered across viewport
const WORD_CONFIG = [
{ text: 'MAC', x: 0.07, y: 0.08, size: 42, rot: -8, weight: 900 },
{ text: 'MBM', x: 0.82, y: 0.12, size: 20, rot: 5, weight: 700 },
{ text: 'AI', x: 0.42, y: 0.05, size: 26, rot: -12, weight: 800 },
{ text: 'MAC', x: 0.91, y: 0.35, size: 34, rot: 3, weight: 900 },
{ text: 'MBM', x: 0.15, y: 0.45, size: 16, rot: -6, weight: 600 },
{ text: 'AI', x: 0.68, y: 0.22, size: 42, rot: 10, weight: 900 },
{ text: 'MAC', x: 0.55, y: 0.72, size: 20, rot: -3, weight: 700 },
{ text: 'MBM', x: 0.28, y: 0.78, size: 26, rot: 7, weight: 800 },
{ text: 'AI', x: 0.78, y: 0.55, size: 16, rot: -14, weight: 600 },
{ text: 'MAC', x: 0.04, y: 0.65, size: 34, rot: 4, weight: 900 },
{ text: 'MBM', x: 0.60, y: 0.88, size: 42, rot: -9, weight: 900 },
{ text: 'AI', x: 0.88, y: 0.78, size: 20, rot: 6, weight: 700 },
{ text: 'MAC', x: 0.35, y: 0.90, size: 16, rot: -5, weight: 600 },
{ text: 'MBM', x: 0.18, y: 0.22, size: 26, rot: 11, weight: 800 },
{ text: 'AI', x: 0.48, y: 0.42, size: 34, rot: -7, weight: 900 },
{ text: 'MAC', x: 0.72, y: 0.60, size: 20, rot: 2, weight: 700 },
{ text: 'MBM', x: 0.95, y: 0.15, size: 16, rot: -13, weight: 600 },
{ text: 'AI', x: 0.08, y: 0.88, size: 26, rot: 9, weight: 800 },
{ text: 'MAC', x: 0.50, y: 0.18, size: 42, rot: -4, weight: 900 },
{ text: 'MBM', x: 0.38, y: 0.55, size: 20, rot: 13, weight: 700 },
{ text: 'CLOUD', x: 0.22, y: 0.35, size: 16, rot: -2, weight: 600 },
{ text: 'GPU', x: 0.75, y: 0.42, size: 16, rot: 8, weight: 600 },
{ text: 'LLM', x: 0.62, y: 0.68, size: 16, rot: -6, weight: 600 },
];
// Physics constants
const REPEL_RADIUS = 200;
const MAX_PUSH = 80;
const SPRING = 0.05;
const FRICTION = 0.82;
const PROXIMITY_GLOW_RADIUS = 160;
function initWords() {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
words = WORD_CONFIG.map((cfg, i) => ({
...cfg,
origX: cfg.x * rect.width,
origY: cfg.y * rect.height,
currX: cfg.x * rect.width,
currY: cfg.y * rect.height,
vx: 0,
vy: 0,
opacity: 0.10,
el: null,
idx: i,
}));
}
function physicsLoop() {
if (!containerEl) return;
let anyMoving = false;
words.forEach(w => {
if (!w.el) return;
// Distance from mouse
const dx = w.currX - mouse.x;
const dy = w.currY - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Proximity glow
let targetOpacity = 0.10;
if (mouse.active && dist < PROXIMITY_GLOW_RADIUS) {
targetOpacity = 0.15 + (1 - dist / PROXIMITY_GLOW_RADIUS) * 0.30;
}
w.opacity += (targetOpacity - w.opacity) * 0.1;
// Repel force from mouse
if (mouse.active && dist < REPEL_RADIUS && dist > 1) {
const force = ((REPEL_RADIUS - dist) / REPEL_RADIUS) * MAX_PUSH;
w.vx += (dx / dist) * force * 0.06;
w.vy += (dy / dist) * force * 0.06;
}
// Spring back to origin
w.vx += (w.origX - w.currX) * SPRING;
w.vy += (w.origY - w.currY) * SPRING;
// Friction
w.vx *= FRICTION;
w.vy *= FRICTION;
// Update position
w.currX += w.vx;
w.currY += w.vy;
// Apply transform
const deltaX = w.currX - w.origX;
const deltaY = w.currY - w.origY;
w.el.style.transform = `rotate(${w.rot}deg) translate(${deltaX}px, ${deltaY}px)`;
w.el.style.opacity = w.opacity;
if (Math.abs(w.vx) > 0.05 || Math.abs(w.vy) > 0.05) anyMoving = true;
});
if (anyMoving || mouse.active) {
rafId = requestAnimationFrame(physicsLoop);
} else {
rafId = null;
}
}
function startPhysics() {
if (!rafId) {
rafId = requestAnimationFrame(physicsLoop);
}
}
function handleMouseMove(e) {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
mouse.active = true;
startPhysics();
}
function handleMouseLeave() {
mouse.x = -9999;
mouse.y = -9999;
mouse.active = false;
startPhysics(); // let spring-back animate
}
function handleTouchMove(e) {
if (!e.touches.length || !containerEl) return;
const rect = containerEl.getBoundingClientRect();
mouse.x = e.touches[0].clientX - rect.left;
mouse.y = e.touches[0].clientY - rect.top;
mouse.active = true;
startPhysics();
}
function handleTouchEnd() {
mouse.x = -9999;
mouse.y = -9999;
mouse.active = false;
startPhysics();
}
function handleResize() {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
words.forEach((w, i) => {
const cfg = WORD_CONFIG[i];
w.origX = cfg.x * rect.width;
w.origY = cfg.y * rect.height;
w.currX = w.origX;
w.currY = w.origY;
w.vx = 0;
w.vy = 0;
});
}
onMount(() => {
initWords();
// Bind word elements
setTimeout(() => {
if (!containerEl) return;
const els = containerEl.querySelectorAll('.pt-word');
els.forEach((el, i) => {
if (words[i]) words[i].el = el;
});
}, 0);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (rafId) cancelAnimationFrame(rafId);
};
});
onDestroy(() => {
if (rafId) cancelAnimationFrame(rafId);
});
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={containerEl}
class="pretext-bg"
aria-hidden="true"
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
on:touchmove|passive={handleTouchMove}
on:touchend={handleTouchEnd}
>
<!-- Scanlines overlay -->
<div class="pt-scanlines"></div>
<!-- Floating words -->
{#each WORD_CONFIG as cfg, i}
<span
class="pt-word"
style="
left: {cfg.x * 100}%;
top: {cfg.y * 100}%;
font-size: {cfg.size}px;
font-weight: {cfg.weight};
transform: rotate({cfg.rot}deg);
opacity: 0.10;
"
>{cfg.text}</span>
{/each}
</div>
<style>
.pretext-bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: auto;
overflow: hidden;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.pt-scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 5px,
rgba(217, 116, 73, 0.018) 6px
);
mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 80%, transparent);
pointer-events: none;
}
.pt-word {
position: absolute;
color: var(--accent, #D97449);
font-family: 'Courier New', monospace;
letter-spacing: 0.15em;
pointer-events: none;
will-change: transform, opacity;
transition: opacity 0.3s ease;
text-transform: uppercase;
}
@media (prefers-reduced-motion: reduce) {
.pt-word {
transition: none !important;
}
}
</style>