Skip to content

Commit e925239

Browse files
committed
feat: homepage MorphingAscii animation and MDX per-page LLM route
- src/components/MorphingAscii.tsx: ASCII art animation that morphs between Cadence code snippets on the landing page hero - src/routes/llms[.]mdx.docs.$.ts: per-page MDX plain-text endpoint at /llms.mdx/<doc-slug> for LLM-friendly single-page context fetching
1 parent 53c8a03 commit e925239

2 files changed

Lines changed: 229 additions & 0 deletions

File tree

src/components/MorphingAscii.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useState, useEffect, useCallback, Suspense } from 'react';
2+
import { Canvas } from '@react-three/fiber';
3+
import { AsciiRenderer, Float, useTexture, OrbitControls, useGLTF } from '@react-three/drei';
4+
import * as THREE from 'three';
5+
import { useTheme } from 'next-themes';
6+
7+
// ----------------------------------------------------
8+
// SCENE COMPONENTS
9+
// ----------------------------------------------------
10+
11+
function FlowCoin() {
12+
const texture = useTexture('/assets/flow-logo.svg');
13+
14+
return (
15+
<group scale={0.85}>
16+
{/* Front Face */}
17+
<mesh position={[0, 0, 0.11]}>
18+
<circleGeometry args={[2, 64]} />
19+
<meshStandardMaterial map={texture} roughness={0.3} metalness={0.8} />
20+
</mesh>
21+
{/* Back Face */}
22+
<mesh position={[0, 0, -0.11]} rotation={[0, Math.PI, 0]}>
23+
<circleGeometry args={[2, 64]} />
24+
<meshStandardMaterial map={texture} roughness={0.3} metalness={0.8} />
25+
</mesh>
26+
{/* Edge */}
27+
<mesh rotation={[Math.PI / 2, 0, 0]}>
28+
<cylinderGeometry args={[2, 2, 0.22, 64, 1, true]} />
29+
<meshStandardMaterial color="#00FF94" roughness={0.3} metalness={0.8} />
30+
</mesh>
31+
</group>
32+
);
33+
}
34+
35+
function CadenceLogo3D() {
36+
const { scene } = useGLTF('/assets/logo.glb');
37+
38+
useEffect(() => {
39+
scene.traverse((child) => {
40+
if (child instanceof THREE.Mesh) {
41+
child.material.roughness = 0.4;
42+
child.material.metalness = 0.6;
43+
}
44+
});
45+
}, [scene]);
46+
47+
return (
48+
<group position={[0, 0, 0]}>
49+
<primitive object={scene} scale={1.2} />
50+
</group>
51+
);
52+
}
53+
54+
function CryptoKitty3D() {
55+
const { scene } = useGLTF('/assets/cryptokitty.glb');
56+
57+
useEffect(() => {
58+
scene.traverse((child) => {
59+
if (child instanceof THREE.Mesh) {
60+
child.material.roughness = 0.5;
61+
child.material.metalness = 0.2;
62+
}
63+
});
64+
}, [scene]);
65+
66+
return (
67+
<group position={[0, -0.4, 0]}>
68+
<primitive object={scene} scale={2.2} />
69+
</group>
70+
);
71+
}
72+
73+
useGLTF.preload('/assets/logo.glb');
74+
useGLTF.preload('/assets/cryptokitty.glb');
75+
76+
function AsciiScene({ activeCycleIdx, fgColor }: { activeCycleIdx: number; fgColor: string }) {
77+
return (
78+
<>
79+
<ambientLight intensity={1.5} />
80+
<directionalLight position={[10, 10, 10]} intensity={3} />
81+
<pointLight position={[-10, -10, -10]} intensity={1} />
82+
83+
<Suspense fallback={null}>
84+
<Float speed={2.5} rotationIntensity={0.2} floatIntensity={0.5}>
85+
<group visible={activeCycleIdx === 0}>
86+
<CadenceLogo3D />
87+
</group>
88+
<group visible={activeCycleIdx === 1}>
89+
<FlowCoin />
90+
</group>
91+
<group visible={activeCycleIdx === 2}>
92+
<CryptoKitty3D />
93+
</group>
94+
</Float>
95+
</Suspense>
96+
97+
{/* Ascii shader layer */}
98+
<AsciiRenderer
99+
fgColor={fgColor}
100+
bgColor="transparent"
101+
characters=" .:'-+*=%@#"
102+
resolution={0.25}
103+
color={false}
104+
invert={false}
105+
/>
106+
</>
107+
);
108+
}
109+
110+
// ----------------------------------------------------
111+
// MAIN COMPONENT
112+
// ----------------------------------------------------
113+
114+
export function MorphingAscii() {
115+
const [activeCycleIdx, setActiveCycleIdx] = useState(0);
116+
const [mounted, setMounted] = useState(false);
117+
const [ready, setReady] = useState(false);
118+
const [isMobile, setIsMobile] = useState(false);
119+
const { resolvedTheme } = useTheme();
120+
121+
const onCreated = useCallback(() => {
122+
// Wait two frames so AsciiRenderer has fully taken over
123+
requestAnimationFrame(() => {
124+
requestAnimationFrame(() => {
125+
setReady(true);
126+
});
127+
});
128+
}, []);
129+
130+
useEffect(() => {
131+
setMounted(true);
132+
const mq = window.matchMedia('(max-width: 1023px)');
133+
setIsMobile(mq.matches);
134+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
135+
mq.addEventListener('change', handler);
136+
return () => mq.removeEventListener('change', handler);
137+
}, []);
138+
139+
useEffect(() => {
140+
const interval = setInterval(() => {
141+
setActiveCycleIdx(prev => (prev + 1) % 3);
142+
}, 6000);
143+
return () => clearInterval(interval);
144+
}, []);
145+
146+
if (!mounted) {
147+
return <div className="w-full h-[280px] lg:h-[700px] lg:w-[700px]" />;
148+
}
149+
150+
return (
151+
<div
152+
className={[
153+
'relative flex flex-col items-center justify-center p-0 m-0 group',
154+
'w-full h-[280px]',
155+
'lg:h-[700px] lg:w-[700px] lg:-right-5',
156+
'overflow-hidden lg:overflow-visible',
157+
isMobile ? '' : 'cursor-grab active:cursor-grabbing',
158+
].join(' ')}
159+
>
160+
{/* 3D ASCII Canvas */}
161+
<div
162+
className="absolute inset-0 overflow-hidden lg:overflow-visible ascii-wrapper"
163+
style={{
164+
opacity: ready ? 1 : 0,
165+
transition: 'opacity 0.3s ease-in',
166+
}}
167+
>
168+
{resolvedTheme === 'light' && (
169+
<style>{`
170+
.ascii-wrapper > div {
171+
font-weight: 900 !important;
172+
}
173+
`}</style>
174+
)}
175+
<Canvas
176+
camera={{ position: [0, 0, 6.5], fov: 50 }}
177+
style={{ touchAction: isMobile ? 'pan-y' : 'none' }}
178+
onCreated={onCreated}
179+
>
180+
<color attach="background" args={['transparent']} />
181+
<AsciiScene
182+
activeCycleIdx={activeCycleIdx}
183+
fgColor={resolvedTheme === 'light' ? '#000000' : '#00FF94'}
184+
/>
185+
<OrbitControls
186+
autoRotate
187+
autoRotateSpeed={6}
188+
enableZoom={false}
189+
enablePan={false}
190+
enableRotate={!isMobile}
191+
/>
192+
</Canvas>
193+
</div>
194+
195+
{/* Mobile: transparent overlay above ascii-wrapper to intercept touches
196+
and pass vertical scroll to the browser natively via pan-y */}
197+
{isMobile && (
198+
<div
199+
style={{
200+
position: 'absolute',
201+
inset: 0,
202+
zIndex: 10,
203+
touchAction: 'pan-y',
204+
}}
205+
/>
206+
)}
207+
</div>
208+
);
209+
}

src/routes/llms[.]mdx.docs.$.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createFileRoute, notFound } from '@tanstack/react-router';
2+
import { source } from '@/lib/source';
3+
4+
export const Route = createFileRoute('/llms.mdx/docs/$')({
5+
server: {
6+
handlers: {
7+
GET: async ({ params }) => {
8+
const slugs = params._splat?.split('/') ?? [];
9+
const page = source.getPage(slugs);
10+
if (!page) throw notFound();
11+
12+
return new Response(await page.data.getText('processed'), {
13+
headers: {
14+
'Content-Type': 'text/markdown',
15+
},
16+
});
17+
},
18+
},
19+
},
20+
});

0 commit comments

Comments
 (0)