MAC / frontend /src /lib /components /MorphIntro.svelte
Aaryan17's picture
chore: upload MAC codebase to HF Space
0e76632 verified
<!-- MorphIntro.svelte — First-time MBM→MAC Devnagri letter morph intro -->
<script>
import { onMount, createEventDispatcher } from 'svelte';
import Loader from './Loader.svelte';
const dispatch = createEventDispatcher();
let canvas;
let ctx;
let phase = 'loading'; // 'loading' | 'morph' | 'done'
let progress = 0;
let opacity = 1;
// Hindi Devnagri characters for MBM and MAC
const MBM_TEXT = 'एम.बी.एम';
const MAC_TEXT = 'एम.ए.सी';
const SUBTITLE = 'MBM AI Cloud';
let animFrame;
let startTime = 0;
const MORPH_DURATION = 3000; // 3 seconds for morph
const HOLD_DURATION = 1500; // hold final state
const FADE_DURATION = 800;
function skipAll() {
cancelAnimationFrame(animFrame);
try { localStorage.setItem('mac_intro_seen', '1'); } catch {}
opacity = 0;
setTimeout(() => dispatch('done'), 300);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function drawFrame(timestamp) {
if (!ctx || !canvas) return;
const w = canvas.width;
const h = canvas.height;
const elapsed = timestamp - startTime;
ctx.clearRect(0, 0, w, h);
// Background
ctx.fillStyle = '#0e0d0c';
ctx.fillRect(0, 0, w, h);
if (elapsed < MORPH_DURATION) {
// Morphing phase
const t = easeInOutCubic(Math.min(elapsed / MORPH_DURATION, 1));
progress = t;
// Draw MBM fading out
const mbmAlpha = 1 - t;
ctx.save();
ctx.globalAlpha = mbmAlpha;
ctx.font = `bold ${Math.round(h * 0.18)}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#E8E6E1';
// Slight upward drift
const mbmY = h * 0.42 - t * 20;
ctx.fillText(MBM_TEXT, w / 2, mbmY);
ctx.restore();
// Draw MAC fading in
const macAlpha = t;
ctx.save();
ctx.globalAlpha = macAlpha;
ctx.font = `bold ${Math.round(h * 0.18)}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#D97449';
const macY = h * 0.42 + (1 - t) * 20;
ctx.fillText(MAC_TEXT, w / 2, macY);
ctx.restore();
// Glitch lines during morph
if (t > 0.1 && t < 0.9) {
const glitchIntensity = Math.sin(t * Math.PI) * 0.5;
for (let i = 0; i < 3; i++) {
const gy = Math.random() * h;
const gw = Math.random() * w * 0.6;
const gx = Math.random() * (w - gw);
ctx.save();
ctx.globalAlpha = glitchIntensity * 0.15;
ctx.fillStyle = i % 2 === 0 ? '#D97449' : '#E8E6E1';
ctx.fillRect(gx, gy, gw, 2);
ctx.restore();
}
}
// Subtitle
ctx.save();
ctx.globalAlpha = t * 0.5;
ctx.font = `${Math.round(h * 0.04)}px 'Inter', sans-serif`;
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.letterSpacing = '0.3em';
ctx.fillText(SUBTITLE, w / 2, h * 0.62);
ctx.restore();
// English transition: MBM → MAC
ctx.save();
const engAlpha = Math.sin(t * Math.PI) * 0.3;
ctx.globalAlpha = engAlpha;
ctx.font = `900 ${Math.round(h * 0.12)}px 'Courier New', monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const engText = t < 0.5 ? 'MBM' : 'MAC';
ctx.fillStyle = t < 0.5 ? 'rgba(232,230,225,0.15)' : 'rgba(217,116,73,0.2)';
ctx.fillText(engText, w / 2, h * 0.78);
ctx.restore();
} else if (elapsed < MORPH_DURATION + HOLD_DURATION) {
// Hold final MAC state
ctx.save();
ctx.font = `bold ${Math.round(h * 0.18)}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#D97449';
ctx.fillText(MAC_TEXT, w / 2, h * 0.42);
ctx.restore();
// Subtitle
ctx.save();
ctx.globalAlpha = 0.5;
ctx.font = `${Math.round(h * 0.04)}px 'Inter', sans-serif`;
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fillText(SUBTITLE, w / 2, h * 0.62);
ctx.restore();
// Pulsing glow
const holdT = (elapsed - MORPH_DURATION) / HOLD_DURATION;
const glowAlpha = 0.08 + Math.sin(holdT * Math.PI * 2) * 0.04;
ctx.save();
const gradient = ctx.createRadialGradient(w/2, h*0.42, 0, w/2, h*0.42, h*0.3);
gradient.addColorStop(0, `rgba(217,116,73,${glowAlpha})`);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
ctx.restore();
} else if (elapsed < MORPH_DURATION + HOLD_DURATION + FADE_DURATION) {
// Fade out
const fadeT = (elapsed - MORPH_DURATION - HOLD_DURATION) / FADE_DURATION;
opacity = 1 - easeInOutCubic(fadeT);
} else {
// Done
try { localStorage.setItem('mac_intro_seen', '1'); } catch {}
dispatch('done');
return;
}
animFrame = requestAnimationFrame(drawFrame);
}
function startMorph() {
phase = 'morph';
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Reset logical dimensions for drawing
canvas.style.width = canvas.offsetWidth + 'px';
canvas.style.height = canvas.offsetHeight + 'px';
startTime = performance.now();
animFrame = requestAnimationFrame(drawFrame);
}
onMount(() => {
// Brief loading pause then start morph
setTimeout(startMorph, 600);
return () => {
if (animFrame) cancelAnimationFrame(animFrame);
};
});
</script>
<div class="morph-overlay" style="opacity: {opacity}" class:hidden={opacity <= 0}>
{#if phase === 'loading'}
<div class="morph-loader">
<Loader size={80} color="#D97449" />
</div>
{/if}
<canvas bind:this={canvas} class="morph-canvas"></canvas>
<button class="morph-skip" on:click={skipAll}>skip ›</button>
</div>
<style>
.morph-overlay {
position: fixed;
inset: 0;
z-index: 10000;
background: #0e0d0c;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.4s ease;
}
.morph-overlay.hidden {
pointer-events: none;
}
.morph-canvas {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
.morph-loader {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.morph-skip {
position: absolute;
bottom: 5%;
right: 4%;
z-index: 3;
font-size: 12px;
color: rgba(255,255,255,0.2);
background: none;
border: none;
cursor: pointer;
letter-spacing: 0.08em;
font-family: inherit;
padding: 4px 8px;
border-radius: 4px;
transition: color 0.2s, background 0.2s;
}
.morph-skip:hover {
color: rgba(255,255,255,0.5);
background: rgba(255,255,255,0.05);
}
</style>